diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 39468209d6..3b2487c16b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,11 +3,9 @@ exclude: |
# NOT INSTALLABLE ADDONS
^base_export_async/|
^base_import_async/|
- ^queue_job/|
^queue_job_cron/|
^queue_job_subscribe/|
^test_base_import_async/|
- ^test_queue_job/|
# END NOT INSTALLABLE ADDONS
# Files and folders generated by bots, to avoid loops
^setup/|/static/description/index\.html$|
diff --git a/queue_job/__init__.py b/queue_job/__init__.py
index 34b2e85788..75f80cf5aa 100644
--- a/queue_job/__init__.py
+++ b/queue_job/__init__.py
@@ -1,5 +1,9 @@
from . import controllers
from . import fields
from . import models
+from . import wizards
from . import jobrunner
from .hooks.post_init_hook import post_init_hook
+
+# shortcuts
+from .job import identity_exact
diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py
index 0b1d16e56e..41dd5bfe66 100644
--- a/queue_job/__manifest__.py
+++ b/queue_job/__manifest__.py
@@ -3,9 +3,9 @@
{
"name": "Job Queue",
- "version": "13.0.3.2.0",
+ "version": "14.0.1.0.0",
"author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
- "website": "https://github.com/OCA/queue/queue_job",
+ "website": "https://github.com/OCA/queue",
"license": "LGPL-3",
"category": "Generic Modules",
"depends": ["mail"],
@@ -14,10 +14,15 @@
"security/security.xml",
"security/ir.model.access.csv",
"views/queue_job_views.xml",
+ "views/queue_job_channel_views.xml",
+ "views/queue_job_function_views.xml",
+ "wizards/queue_jobs_to_done_views.xml",
+ "wizards/queue_requeue_job_views.xml",
+ "views/queue_job_menus.xml",
"data/queue_data.xml",
"data/queue_job_function_data.xml",
],
- "installable": False,
+ "installable": True,
"development_status": "Mature",
"maintainers": ["guewen"],
"post_init_hook": "post_init_hook",
diff --git a/queue_job/exception.py b/queue_job/exception.py
index 41f89991c4..093344ed3d 100644
--- a/queue_job/exception.py
+++ b/queue_job/exception.py
@@ -29,7 +29,7 @@ class RetryableJobError(JobError):
"""
def __init__(self, msg, seconds=None, ignore_retry=False):
- super(RetryableJobError, self).__init__(msg)
+ super().__init__(msg)
self.seconds = seconds
self.ignore_retry = ignore_retry
diff --git a/queue_job/fields.py b/queue_job/fields.py
index 2658e01c73..50183993d8 100644
--- a/queue_job/fields.py
+++ b/queue_job/fields.py
@@ -25,7 +25,7 @@ class JobSerialized(fields.Field):
type = "job_serialized"
column_type = ("text", "text")
- _slots = {"_base_type": type}
+ _base_type = None
# these are the default values when we convert an empty value
_default_json_mapping = {
@@ -97,7 +97,7 @@ class JobDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
env = kwargs.pop("env")
- super(JobDecoder, self).__init__(object_hook=self.object_hook, *args, **kwargs)
+ super().__init__(object_hook=self.object_hook, *args, **kwargs)
assert env
self.env = env
diff --git a/queue_job/job.py b/queue_job/job.py
index 568808c002..06eb6c0516 100644
--- a/queue_job/job.py
+++ b/queue_job/job.py
@@ -1,7 +1,6 @@
# Copyright 2013-2020 Camptocamp
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-import functools
import hashlib
import inspect
import logging
@@ -392,7 +391,7 @@ def __init__(
channel=None,
identity_key=None,
):
- """ Create a Job
+ """Create a Job
:param func: function to execute
:type func: function
@@ -684,9 +683,6 @@ def __repr__(self):
def _get_retry_seconds(self, seconds=None):
retry_pattern = self.job_config.retry_pattern
- if not retry_pattern:
- # TODO deprecated by :job-no-decorator:
- retry_pattern = getattr(self.func, "retry_pattern", None)
if not seconds and retry_pattern:
# ordered from higher to lower count of retries
patt = sorted(retry_pattern.items(), key=lambda t: t[0])
@@ -719,13 +715,6 @@ def related_action(self):
return None
funcname = self.job_config.related_action_func_name
- if not funcname and hasattr(self.func, "related_action"):
- # TODO deprecated by :job-no-decorator:
- funcname = self.func.related_action
- # decorator is set but empty: disable the default one
- if not funcname:
- return None
-
if not funcname:
funcname = record._default_related_action
if not isinstance(funcname, str):
@@ -735,9 +724,6 @@ def related_action(self):
)
action = getattr(record, funcname)
action_kwargs = self.job_config.related_action_kwargs
- if not action_kwargs:
- # TODO deprecated by :job-no-decorator:
- action_kwargs = getattr(self.func, "kwargs", {})
return action(**action_kwargs)
@@ -745,241 +731,3 @@ def _is_model_method(func):
return inspect.ismethod(func) and isinstance(
func.__self__.__class__, odoo.models.MetaModel
)
-
-
-# TODO deprecated by :job-no-decorator:
-def job(func=None, default_channel="root", retry_pattern=None):
- """Decorator for job methods.
-
- Deprecated. Use ``queue.job.function`` XML records (details in
- ``readme/USAGE.rst``).
-
- It enables the possibility to use a Model's method as a job function.
-
- Optional argument:
-
- :param default_channel: the channel wherein the job will be assigned. This
- channel is set at the installation of the module
- and can be manually changed later using the views.
- :param retry_pattern: The retry pattern to use for postponing a job.
- If a job is postponed and there is no eta
- specified, the eta will be determined from the
- dict in retry_pattern. When no retry pattern
- is provided, jobs will be retried after
- :const:`RETRY_INTERVAL` seconds.
- :type retry_pattern: dict(retry_count,retry_eta_seconds)
-
- Indicates that a method of a Model can be delayed in the Job Queue.
-
- When a method has the ``@job`` decorator, its calls can then be delayed
- with::
-
- recordset.with_delay(priority=10).the_method(args, **kwargs)
-
- Where ``the_method`` is the method decorated with ``@job``. Its arguments
- and keyword arguments will be kept in the Job Queue for its asynchronous
- execution.
-
- ``default_channel`` indicates in which channel the job must be executed
-
- ``retry_pattern`` is a dict where keys are the count of retries and the
- values are the delay to postpone a job.
-
- Example:
-
- .. code-block:: python
-
- class ProductProduct(models.Model):
- _inherit = 'product.product'
-
- @job
- def export_one_thing(self, one_thing):
- # work
- # export one_thing
-
- # [...]
-
- env['a.model'].export_one_thing(the_thing_to_export)
- # => normal and synchronous function call
-
- env['a.model'].with_delay().export_one_thing(the_thing_to_export)
- # => the job will be executed as soon as possible
-
- delayable = env['a.model'].with_delay(priority=30, eta=60*60*5)
- delayable.export_one_thing(the_thing_to_export)
- # => the job will be executed with a low priority and not before a
- # delay of 5 hours from now
-
- @job(default_channel='root.subchannel')
- def export_one_thing(one_thing):
- # work
- # export one_thing
-
- @job(retry_pattern={1: 10 * 60,
- 5: 20 * 60,
- 10: 30 * 60,
- 15: 12 * 60 * 60})
- def retryable_example():
- # 5 first retries postponed 10 minutes later
- # retries 5 to 10 postponed 20 minutes later
- # retries 10 to 15 postponed 30 minutes later
- # all subsequent retries postponed 12 hours later
- raise RetryableJobError('Must be retried later')
-
- env['a.model'].with_delay().retryable_example()
-
-
- See also: :py:func:`related_action` a related action can be attached
- to a job
- """
- if func is None:
- return functools.partial(
- job, default_channel=default_channel, retry_pattern=retry_pattern
- )
-
- xml_fields = [
- ' \n'
- ' _test_job\n'
- ]
- if default_channel:
- xml_fields.append(' ')
- if retry_pattern:
- xml_fields.append(' {retry_pattern}')
-
- _logger.info(
- "@job is deprecated and no longer needed (on %s), it is advised to use an "
- "XML record (activate DEBUG log for snippet)",
- func.__name__,
- )
- if _logger.isEnabledFor(logging.DEBUG):
- xml_record = (
- '\n' + "\n".join(xml_fields) + "\n"
- ).format(**{"method": func.__name__, "retry_pattern": retry_pattern})
- _logger.debug(
- "XML snippet (to complete) for replacing @job on %s:\n%s",
- func.__name__,
- xml_record,
- )
-
- def delay_from_model(*args, **kwargs):
- raise AttributeError(
- "method.delay() can no longer be used, the general form is "
- "env['res.users'].with_delay().method()"
- )
-
- assert default_channel == "root" or default_channel.startswith(
- "root."
- ), "The channel path must start by 'root'"
- assert retry_pattern is None or isinstance(
- retry_pattern, dict
- ), "retry_pattern must be a dict"
-
- delay_func = delay_from_model
-
- func.delayable = True
- func.delay = delay_func
- func.retry_pattern = retry_pattern
- func.default_channel = default_channel
- return func
-
-
-# TODO deprecated by :job-no-decorator:
-def related_action(action=None, **kwargs):
- """Attach a *Related Action* to a job (decorator)
-
- Deprecated. Use ``queue.job.function`` XML records (details in
- ``readme/USAGE.rst``).
-
- A *Related Action* will appear as a button on the Odoo view.
- The button will execute the action, usually it will open the
- form view of the record related to the job.
-
- The ``action`` must be a method on the `queue.job` model.
-
- Example usage:
-
- .. code-block:: python
-
- class QueueJob(models.Model):
- _inherit = 'queue.job'
-
- def related_action_partner(self):
- self.ensure_one()
- model = self.model_name
- partner = self.records
- # possibly get the real ID if partner_id is a binding ID
- action = {
- 'name': _("Partner"),
- 'type': 'ir.actions.act_window',
- 'res_model': model,
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_id': partner.id,
- }
- return action
-
- class ResPartner(models.Model):
- _inherit = 'res.partner'
-
- @job
- @related_action(action='related_action_partner')
- def export_partner(self):
- # ...
-
- The kwargs are transmitted to the action:
-
- .. code-block:: python
-
- class QueueJob(models.Model):
- _inherit = 'queue.job'
-
- def related_action_product(self, extra_arg=1):
- assert extra_arg == 2
- model = self.model_name
- ...
-
- class ProductProduct(models.Model):
- _inherit = 'product.product'
-
- @job
- @related_action(action='related_action_product', extra_arg=2)
- def export_product(self):
- # ...
-
- """
-
- def decorate(func):
- related_action_dict = {
- "func_name": action,
- }
- if kwargs:
- related_action_dict["kwargs"] = kwargs
-
- xml_fields = (
- ' \n'
- ' _test_job\n'
- ' {related_action}'
- )
-
- _logger.info(
- "@related_action is deprecated and no longer needed (on %s),"
- " it is advised to use an XML record (activate DEBUG log for snippet)",
- func.__name__,
- )
- if _logger.isEnabledFor(logging.DEBUG):
- xml_record = (
- '\n' + xml_fields + "\n"
- ).format(**{"method": func.__name__, "related_action": action})
- _logger.debug(
- "XML snippet (to complete) for replacing @related_action on %s:\n%s",
- func.__name__,
- xml_record,
- )
-
- func.related_action = action
- func.kwargs = kwargs
- return func
-
- return decorate
diff --git a/queue_job/jobrunner/__init__.py b/queue_job/jobrunner/__init__.py
index 9de83aeeb6..7ad479ff33 100644
--- a/queue_job/jobrunner/__init__.py
+++ b/queue_job/jobrunner/__init__.py
@@ -51,7 +51,7 @@ class WorkerJobRunner(server.Worker):
""" Jobrunner workers """
def __init__(self, multi):
- super(WorkerJobRunner, self).__init__(multi)
+ super().__init__(multi)
self.watchdog_timeout = None
self.runner = QueueJobRunner.from_environ_or_config()
@@ -60,7 +60,7 @@ def sleep(self):
def signal_handler(self, sig, frame):
_logger.debug("WorkerJobRunner (%s) received signal %s", self.pid, sig)
- super(WorkerJobRunner, self).signal_handler(sig, frame)
+ super().signal_handler(sig, frame)
self.runner.stop()
def process_work(self):
diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py
index 4a08f9f874..1f42a10cc9 100644
--- a/queue_job/jobrunner/channels.py
+++ b/queue_job/jobrunner/channels.py
@@ -117,7 +117,7 @@ class SafeSet(set):
def remove(self, o):
# pylint: disable=missing-return,except-pass
try:
- super(SafeSet, self).remove(o)
+ super().remove(o)
except KeyError:
pass
diff --git a/queue_job/migrations/13.0.1.2.0/post-migrate-queue-job.py b/queue_job/migrations/13.0.1.2.0/post-migrate-queue-job.py
deleted file mode 100644
index 11ddf11737..0000000000
--- a/queue_job/migrations/13.0.1.2.0/post-migrate-queue-job.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-
-from odoo import SUPERUSER_ID, api
-
-
-def migrate(cr, version):
- with api.Environment.manage():
- env = api.Environment(cr, SUPERUSER_ID, {})
- rule = env.ref("queue_job.queue_job_comp_rule", raise_if_not_found=False)
- if rule:
- domain = """[
- '|',
- ('company_id', '=', False),
- ('company_id', 'in', company_ids)
- ]"""
- values = {
- "domain_force": domain,
- }
- rule.write(values)
diff --git a/queue_job/migrations/13.0.2.0.0/post-migration.py b/queue_job/migrations/13.0.2.0.0/post-migration.py
deleted file mode 100644
index a399e53e36..0000000000
--- a/queue_job/migrations/13.0.2.0.0/post-migration.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-
-import logging
-
-from odoo import SUPERUSER_ID, api, exceptions
-
-_logger = logging.getLogger(__name__)
-
-
-def migrate(cr, version):
- with api.Environment.manage():
- env = api.Environment(cr, SUPERUSER_ID, {})
- for job_func in env["queue.job.function"].search([]):
- try:
- # trigger inverse field to set model_id and method
- job_func.name = job_func.name
- except exceptions.UserError:
- # ignore invalid entries not to block migration
- _logger.error(
- "could not migrate job function '%s' (id: %s), invalid name",
- job_func.name,
- job_func.id,
- )
diff --git a/queue_job/migrations/13.0.3.2.0/pre-migration.py b/queue_job/migrations/13.0.3.2.0/pre-migration.py
deleted file mode 100644
index 897846fa83..0000000000
--- a/queue_job/migrations/13.0.3.2.0/pre-migration.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-
-import logging
-
-from odoo.tools.sql import column_exists
-
-_logger = logging.getLogger(__name__)
-
-
-def migrate(cr, version):
- if not column_exists(cr, "queue_job", "records"):
- cr.execute(
- """
- ALTER TABLE queue_job
- ADD COLUMN records text;
- """
- )
- cr.execute(
- """
- UPDATE queue_job
- SET records = '{"_type": "odoo_recordset"'
- || ', "model": "' || model_name || '"'
- || ', "uid": ' || user_id
- || ', "ids": ' || record_ids
- || '}'
- WHERE records IS NULL;
- """
- )
diff --git a/queue_job/models/__init__.py b/queue_job/models/__init__.py
index b7b1791462..4744e7ab46 100644
--- a/queue_job/models/__init__.py
+++ b/queue_job/models/__init__.py
@@ -1,3 +1,5 @@
from . import base
from . import ir_model_fields
from . import queue_job
+from . import queue_job_channel
+from . import queue_job_function
diff --git a/queue_job/models/base.py b/queue_job/models/base.py
index 3bb4d78361..a05da489b3 100644
--- a/queue_job/models/base.py
+++ b/queue_job/models/base.py
@@ -1,7 +1,6 @@
# Copyright 2016 Camptocamp
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-import inspect
import logging
import os
@@ -21,20 +20,6 @@ class Base(models.AbstractModel):
_inherit = "base"
- # TODO deprecated by :job-no-decorator:
- def _register_hook(self):
- """Register marked jobs"""
- super(Base, self)._register_hook()
- job_methods = [
- method
- for __, method in inspect.getmembers(
- self.__class__, predicate=inspect.isfunction
- )
- if getattr(method, "delayable", None)
- ]
- for job_method in job_methods:
- self.env["queue.job.function"]._register_job(self, job_method)
-
def with_delay(
self,
priority=None,
@@ -44,7 +29,7 @@ def with_delay(
channel=None,
identity_key=None,
):
- """ Return a ``DelayableRecordset``
+ """Return a ``DelayableRecordset``
The returned instance allows to enqueue any method of the recordset's
Model.
diff --git a/queue_job/models/ir_model_fields.py b/queue_job/models/ir_model_fields.py
index 30d48dc236..5a31fcdc5f 100644
--- a/queue_job/models/ir_model_fields.py
+++ b/queue_job/models/ir_model_fields.py
@@ -7,4 +7,7 @@
class IrModelFields(models.Model):
_inherit = "ir.model.fields"
- ttype = fields.Selection(selection_add=[("job_serialized", "Job Serialized")])
+ ttype = fields.Selection(
+ selection_add=[("job_serialized", "Job Serialized")],
+ ondelete={"job_serialized": "cascade"},
+ )
diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py
index 1da0eaf86d..9376fab07d 100644
--- a/queue_job/models/queue_job.py
+++ b/queue_job/models/queue_job.py
@@ -1,13 +1,10 @@
-# Copyright 2013-2016 Camptocamp SA
+# Copyright 2013-2020 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
-import ast
import logging
-import re
-from collections import namedtuple
from datetime import datetime, timedelta
-from odoo import _, api, exceptions, fields, models, tools
+from odoo import _, api, exceptions, fields, models
from odoo.osv import expression
from ..fields import JobSerialized
@@ -16,9 +13,6 @@
_logger = logging.getLogger(__name__)
-regex_job_function_name = re.compile(r"^<([0-9a-z_\.]+)>\.([0-9a-zA-Z_]+)$")
-
-
class QueueJob(models.Model):
"""Model storing the jobs to be executed."""
@@ -69,7 +63,9 @@ class QueueJob(models.Model):
# actions), can be removed (replaced by "records") in 14.0
record_ids = JobSerialized(compute="_compute_record_ids", base_type=list)
records = JobSerialized(
- string="Record(s)", readonly=True, base_type=models.BaseModel,
+ string="Record(s)",
+ readonly=True,
+ base_type=models.BaseModel,
)
args = JobSerialized(readonly=True, base_type=tuple)
kwargs = JobSerialized(readonly=True, base_type=dict)
@@ -248,7 +244,7 @@ def _message_post_on_failure(self):
for record in self:
msg = record._message_failed_job()
if msg:
- record.message_post(body=msg, subtype="queue_job.mt_job_failed")
+ record.message_post(body=msg, subtype_xmlid="queue_job.mt_job_failed")
def _subscribe_users_domain(self):
"""Subscribe all users having the 'Queue Job Manager' group"""
@@ -381,376 +377,3 @@ def related_action_open_record(self):
def _test_job(self):
_logger.info("Running test job.")
-
-
-class RequeueJob(models.TransientModel):
- _name = "queue.requeue.job"
- _description = "Wizard to requeue a selection of jobs"
-
- def _default_job_ids(self):
- res = False
- context = self.env.context
- if context.get("active_model") == "queue.job" and context.get("active_ids"):
- res = context["active_ids"]
- return res
-
- job_ids = fields.Many2many(
- comodel_name="queue.job", string="Jobs", default=lambda r: r._default_job_ids()
- )
-
- def requeue(self):
- jobs = self.job_ids
- jobs.requeue()
- return {"type": "ir.actions.act_window_close"}
-
-
-class SetJobsToDone(models.TransientModel):
- _inherit = "queue.requeue.job"
- _name = "queue.jobs.to.done"
- _description = "Set all selected jobs to done"
-
- def set_done(self):
- jobs = self.job_ids
- jobs.button_done()
- return {"type": "ir.actions.act_window_close"}
-
-
-class JobChannel(models.Model):
- _name = "queue.job.channel"
- _description = "Job Channels"
-
- name = fields.Char()
- complete_name = fields.Char(
- compute="_compute_complete_name", store=True, readonly=True
- )
- parent_id = fields.Many2one(
- comodel_name="queue.job.channel", string="Parent Channel", ondelete="restrict"
- )
- job_function_ids = fields.One2many(
- comodel_name="queue.job.function",
- inverse_name="channel_id",
- string="Job Functions",
- )
- removal_interval = fields.Integer(
- default=lambda self: self.env["queue.job"]._removal_interval, required=True
- )
-
- _sql_constraints = [
- ("name_uniq", "unique(complete_name)", "Channel complete name must be unique")
- ]
-
- @api.depends("name", "parent_id.complete_name")
- def _compute_complete_name(self):
- for record in self:
- if not record.name:
- complete_name = "" # new record
- elif record.parent_id:
- complete_name = ".".join([record.parent_id.complete_name, record.name])
- else:
- complete_name = record.name
- record.complete_name = complete_name
-
- @api.constrains("parent_id", "name")
- def parent_required(self):
- for record in self:
- if record.name != "root" and not record.parent_id:
- raise exceptions.ValidationError(_("Parent channel required."))
-
- @api.model_create_multi
- def create(self, vals_list):
- records = self.browse()
- if self.env.context.get("install_mode"):
- # installing a module that creates a channel: rebinds the channel
- # to an existing one (likely we already had the channel created by
- # the @job decorator previously)
- new_vals_list = []
- for vals in vals_list:
- name = vals.get("name")
- parent_id = vals.get("parent_id")
- if name and parent_id:
- existing = self.search(
- [("name", "=", name), ("parent_id", "=", parent_id)]
- )
- if existing:
- if not existing.get_metadata()[0].get("noupdate"):
- existing.write(vals)
- records |= existing
- continue
- new_vals_list.append(vals)
- vals_list = new_vals_list
- records |= super().create(vals_list)
- return records
-
- def write(self, values):
- for channel in self:
- if (
- not self.env.context.get("install_mode")
- and channel.name == "root"
- and ("name" in values or "parent_id" in values)
- ):
- raise exceptions.UserError(_("Cannot change the root channel"))
- return super(JobChannel, self).write(values)
-
- def unlink(self):
- for channel in self:
- if channel.name == "root":
- raise exceptions.UserError(_("Cannot remove the root channel"))
- return super(JobChannel, self).unlink()
-
- def name_get(self):
- result = []
- for record in self:
- result.append((record.id, record.complete_name))
- return result
-
-
-class JobFunction(models.Model):
- _name = "queue.job.function"
- _description = "Job Functions"
- _log_access = False
-
- JobConfig = namedtuple(
- "JobConfig",
- "channel "
- "retry_pattern "
- "related_action_enable "
- "related_action_func_name "
- "related_action_kwargs ",
- )
-
- def _default_channel(self):
- return self.env.ref("queue_job.channel_root")
-
- name = fields.Char(
- compute="_compute_name", inverse="_inverse_name", index=True, store=True,
- )
-
- # 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"
- )
- method = fields.Char()
-
- channel_id = fields.Many2one(
- comodel_name="queue.job.channel",
- string="Channel",
- required=True,
- default=lambda r: r._default_channel(),
- )
- channel = fields.Char(related="channel_id.complete_name", store=True, readonly=True)
- retry_pattern = JobSerialized(string="Retry Pattern (serialized)", base_type=dict)
- edit_retry_pattern = fields.Text(
- string="Retry Pattern",
- compute="_compute_edit_retry_pattern",
- inverse="_inverse_edit_retry_pattern",
- help="Pattern expressing from the count of retries on retryable errors,"
- " the number of of seconds to postpone the next execution.\n"
- "Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
- "See the module description for details.",
- )
- related_action = JobSerialized(string="Related Action (serialized)", base_type=dict)
- edit_related_action = fields.Text(
- string="Related Action",
- compute="_compute_edit_related_action",
- inverse="_inverse_edit_related_action",
- help="The action when the button *Related Action* is used on a job. "
- "The default action is to open the view of the record related "
- "to the job. Configured as a dictionary with optional keys: "
- "enable, func_name, kwargs.\n"
- "See the module description for details.",
- )
-
- @api.depends("model_id.model", "method")
- def _compute_name(self):
- for record in self:
- if not (record.model_id and record.method):
- record.name = ""
- continue
- record.name = self.job_function_name(record.model_id.model, record.method)
-
- def _inverse_name(self):
- groups = regex_job_function_name.match(self.name)
- if not groups:
- raise exceptions.UserError(_("Invalid job function: {}").format(self.name))
- model_name = groups[1]
- method = groups[2]
- model = self.env["ir.model"].search([("model", "=", model_name)], limit=1)
- if not model:
- raise exceptions.UserError(_("Model {} not found").format(model_name))
- self.model_id = model.id
- self.method = method
-
- @api.depends("retry_pattern")
- def _compute_edit_retry_pattern(self):
- for record in self:
- retry_pattern = record._parse_retry_pattern()
- record.edit_retry_pattern = str(retry_pattern)
-
- def _inverse_edit_retry_pattern(self):
- try:
- self.retry_pattern = ast.literal_eval(self.edit_retry_pattern or "{}")
- except (ValueError, TypeError):
- raise exceptions.UserError(self._retry_pattern_format_error_message())
-
- @api.depends("related_action")
- def _compute_edit_related_action(self):
- for record in self:
- record.edit_related_action = str(record.related_action)
-
- def _inverse_edit_related_action(self):
- try:
- self.related_action = ast.literal_eval(self.edit_related_action or "{}")
- except (ValueError, TypeError):
- raise exceptions.UserError(self._related_action_format_error_message())
-
- @staticmethod
- def job_function_name(model_name, method_name):
- return "<{}>.{}".format(model_name, method_name)
-
- # TODO deprecated by :job-no-decorator:
- def _find_or_create_channel(self, channel_path):
- channel_model = self.env["queue.job.channel"]
- parts = channel_path.split(".")
- parts.reverse()
- channel_name = parts.pop()
- assert channel_name == "root", "A channel path starts with 'root'"
- # get the root channel
- channel = channel_model.search([("name", "=", channel_name)])
- while parts:
- channel_name = parts.pop()
- parent_channel = channel
- channel = channel_model.search(
- [("name", "=", channel_name), ("parent_id", "=", parent_channel.id)],
- limit=1,
- )
- if not channel:
- channel = channel_model.create(
- {"name": channel_name, "parent_id": parent_channel.id}
- )
- return channel
-
- def job_default_config(self):
- return self.JobConfig(
- channel="root",
- retry_pattern={},
- related_action_enable=True,
- related_action_func_name=None,
- related_action_kwargs={},
- )
-
- def _parse_retry_pattern(self):
- try:
- # as json can't have integers as keys and the field is stored
- # as json, convert back to int
- retry_pattern = {
- int(try_count): postpone_seconds
- for try_count, postpone_seconds in self.retry_pattern.items()
- }
- except ValueError:
- _logger.error(
- "Invalid retry pattern for job function %s,"
- " keys could not be parsed as integers, fallback"
- " to the default retry pattern.",
- self.name,
- )
- retry_pattern = {}
- return retry_pattern
-
- @tools.ormcache("name")
- def job_config(self, name):
- config = self.search([("name", "=", name)], limit=1)
- if not config:
- return self.job_default_config()
- retry_pattern = config._parse_retry_pattern()
- return self.JobConfig(
- channel=config.channel,
- retry_pattern=retry_pattern,
- related_action_enable=config.related_action.get("enable", True),
- related_action_func_name=config.related_action.get("func_name"),
- related_action_kwargs=config.related_action.get("kwargs"),
- )
-
- def _retry_pattern_format_error_message(self):
- return _(
- "Unexpected format of Retry Pattern for {}.\n"
- "Example of valid format:\n"
- "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
- ).format(self.name)
-
- @api.constrains("retry_pattern")
- def _check_retry_pattern(self):
- for record in self:
- retry_pattern = record.retry_pattern
- if not retry_pattern:
- continue
-
- all_values = list(retry_pattern) + list(retry_pattern.values())
- for value in all_values:
- try:
- int(value)
- except ValueError:
- raise exceptions.UserError(
- record._retry_pattern_format_error_message()
- )
-
- def _related_action_format_error_message(self):
- return _(
- "Unexpected format of Related Action for {}.\n"
- "Example of valid format:\n"
- '{{"enable": True, "func_name": "related_action_foo",'
- ' "kwargs" {{"limit": 10}}}}'
- ).format(self.name)
-
- @api.constrains("related_action")
- def _check_related_action(self):
- valid_keys = ("enable", "func_name", "kwargs")
- for record in self:
- related_action = record.related_action
- if not related_action:
- continue
-
- if any(key not in valid_keys for key in related_action):
- raise exceptions.UserError(
- record._related_action_format_error_message()
- )
-
- @api.model_create_multi
- def create(self, vals_list):
- records = self.browse()
- if self.env.context.get("install_mode"):
- # installing a module that creates a job function: rebinds the record
- # to an existing one (likely we already had the job function created by
- # the @job decorator previously)
- new_vals_list = []
- for vals in vals_list:
- name = vals.get("name")
- if name:
- existing = self.search([("name", "=", name)], limit=1)
- if existing:
- if not existing.get_metadata()[0].get("noupdate"):
- existing.write(vals)
- records |= existing
- continue
- new_vals_list.append(vals)
- vals_list = new_vals_list
- records |= super().create(vals_list)
- self.clear_caches()
- return records
-
- def write(self, values):
- res = super().write(values)
- self.clear_caches()
- return res
-
- def unlink(self):
- res = super().unlink()
- self.clear_caches()
- return res
-
- # TODO deprecated by :job-no-decorator:
- def _register_job(self, model, job_method):
- func_name = self.job_function_name(model._name, job_method.__name__)
- if not self.search_count([("name", "=", func_name)]):
- channel = self._find_or_create_channel(job_method.default_channel)
- self.create({"name": func_name, "channel_id": channel.id})
diff --git a/queue_job/models/queue_job_channel.py b/queue_job/models/queue_job_channel.py
new file mode 100644
index 0000000000..374e7417f7
--- /dev/null
+++ b/queue_job/models/queue_job_channel.py
@@ -0,0 +1,94 @@
+# Copyright 2013-2020 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+
+from odoo import _, api, exceptions, fields, models
+
+
+class QueueJobChannel(models.Model):
+ _name = "queue.job.channel"
+ _description = "Job Channels"
+
+ name = fields.Char()
+ complete_name = fields.Char(
+ compute="_compute_complete_name", store=True, readonly=True
+ )
+ parent_id = fields.Many2one(
+ comodel_name="queue.job.channel", string="Parent Channel", ondelete="restrict"
+ )
+ job_function_ids = fields.One2many(
+ comodel_name="queue.job.function",
+ inverse_name="channel_id",
+ string="Job Functions",
+ )
+ removal_interval = fields.Integer(
+ default=lambda self: self.env["queue.job"]._removal_interval, required=True
+ )
+
+ _sql_constraints = [
+ ("name_uniq", "unique(complete_name)", "Channel complete name must be unique")
+ ]
+
+ @api.depends("name", "parent_id.complete_name")
+ def _compute_complete_name(self):
+ for record in self:
+ if not record.name:
+ complete_name = "" # new record
+ elif record.parent_id:
+ complete_name = ".".join([record.parent_id.complete_name, record.name])
+ else:
+ complete_name = record.name
+ record.complete_name = complete_name
+
+ @api.constrains("parent_id", "name")
+ def parent_required(self):
+ for record in self:
+ if record.name != "root" and not record.parent_id:
+ raise exceptions.ValidationError(_("Parent channel required."))
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = self.browse()
+ if self.env.context.get("install_mode"):
+ # installing a module that creates a channel: rebinds the channel
+ # to an existing one (likely we already had the channel created by
+ # the @job decorator previously)
+ new_vals_list = []
+ for vals in vals_list:
+ name = vals.get("name")
+ parent_id = vals.get("parent_id")
+ if name and parent_id:
+ existing = self.search(
+ [("name", "=", name), ("parent_id", "=", parent_id)]
+ )
+ if existing:
+ if not existing.get_metadata()[0].get("noupdate"):
+ existing.write(vals)
+ records |= existing
+ continue
+ new_vals_list.append(vals)
+ vals_list = new_vals_list
+ records |= super().create(vals_list)
+ return records
+
+ def write(self, values):
+ for channel in self:
+ if (
+ not self.env.context.get("install_mode")
+ and channel.name == "root"
+ and ("name" in values or "parent_id" in values)
+ ):
+ raise exceptions.UserError(_("Cannot change the root channel"))
+ return super().write(values)
+
+ def unlink(self):
+ for channel in self:
+ if channel.name == "root":
+ raise exceptions.UserError(_("Cannot remove the root channel"))
+ return super().unlink()
+
+ def name_get(self):
+ result = []
+ for record in self:
+ result.append((record.id, record.complete_name))
+ return result
diff --git a/queue_job/models/queue_job_function.py b/queue_job/models/queue_job_function.py
new file mode 100644
index 0000000000..ef6c1b849d
--- /dev/null
+++ b/queue_job/models/queue_job_function.py
@@ -0,0 +1,250 @@
+# Copyright 2013-2020 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+import ast
+import logging
+import re
+from collections import namedtuple
+
+from odoo import _, api, exceptions, fields, models, tools
+
+from ..fields import JobSerialized
+
+_logger = logging.getLogger(__name__)
+
+
+regex_job_function_name = re.compile(r"^<([0-9a-z_\.]+)>\.([0-9a-zA-Z_]+)$")
+
+
+class QueueJobFunction(models.Model):
+ _name = "queue.job.function"
+ _description = "Job Functions"
+ _log_access = False
+
+ JobConfig = namedtuple(
+ "JobConfig",
+ "channel "
+ "retry_pattern "
+ "related_action_enable "
+ "related_action_func_name "
+ "related_action_kwargs ",
+ )
+
+ def _default_channel(self):
+ return self.env.ref("queue_job.channel_root")
+
+ name = fields.Char(
+ compute="_compute_name",
+ inverse="_inverse_name",
+ index=True,
+ store=True,
+ )
+
+ # 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"
+ )
+ method = fields.Char()
+
+ channel_id = fields.Many2one(
+ comodel_name="queue.job.channel",
+ string="Channel",
+ required=True,
+ default=lambda r: r._default_channel(),
+ )
+ channel = fields.Char(related="channel_id.complete_name", store=True, readonly=True)
+ retry_pattern = JobSerialized(string="Retry Pattern (serialized)", base_type=dict)
+ edit_retry_pattern = fields.Text(
+ string="Retry Pattern",
+ compute="_compute_edit_retry_pattern",
+ inverse="_inverse_edit_retry_pattern",
+ help="Pattern expressing from the count of retries on retryable errors,"
+ " the number of of seconds to postpone the next execution.\n"
+ "Example: {1: 10, 5: 20, 10: 30, 15: 300}.\n"
+ "See the module description for details.",
+ )
+ related_action = JobSerialized(string="Related Action (serialized)", base_type=dict)
+ edit_related_action = fields.Text(
+ string="Related Action",
+ compute="_compute_edit_related_action",
+ inverse="_inverse_edit_related_action",
+ help="The action when the button *Related Action* is used on a job. "
+ "The default action is to open the view of the record related "
+ "to the job. Configured as a dictionary with optional keys: "
+ "enable, func_name, kwargs.\n"
+ "See the module description for details.",
+ )
+
+ @api.depends("model_id.model", "method")
+ def _compute_name(self):
+ for record in self:
+ if not (record.model_id and record.method):
+ record.name = ""
+ continue
+ record.name = self.job_function_name(record.model_id.model, record.method)
+
+ def _inverse_name(self):
+ groups = regex_job_function_name.match(self.name)
+ if not groups:
+ raise exceptions.UserError(_("Invalid job function: {}").format(self.name))
+ model_name = groups[1]
+ method = groups[2]
+ model = self.env["ir.model"].search([("model", "=", model_name)], limit=1)
+ if not model:
+ raise exceptions.UserError(_("Model {} not found").format(model_name))
+ self.model_id = model.id
+ self.method = method
+
+ @api.depends("retry_pattern")
+ def _compute_edit_retry_pattern(self):
+ for record in self:
+ retry_pattern = record._parse_retry_pattern()
+ record.edit_retry_pattern = str(retry_pattern)
+
+ def _inverse_edit_retry_pattern(self):
+ try:
+ edited = (self.edit_retry_pattern or "").strip()
+ if edited:
+ self.retry_pattern = ast.literal_eval(edited)
+ else:
+ self.retry_pattern = {}
+ except (ValueError, TypeError, SyntaxError):
+ raise exceptions.UserError(self._retry_pattern_format_error_message())
+
+ @api.depends("related_action")
+ def _compute_edit_related_action(self):
+ for record in self:
+ record.edit_related_action = str(record.related_action)
+
+ def _inverse_edit_related_action(self):
+ try:
+ edited = (self.edit_related_action or "").strip()
+ if edited:
+ self.related_action = ast.literal_eval(edited)
+ else:
+ self.related_action = {}
+ except (ValueError, TypeError, SyntaxError):
+ raise exceptions.UserError(self._related_action_format_error_message())
+
+ @staticmethod
+ def job_function_name(model_name, method_name):
+ return "<{}>.{}".format(model_name, method_name)
+
+ def job_default_config(self):
+ return self.JobConfig(
+ channel="root",
+ retry_pattern={},
+ related_action_enable=True,
+ related_action_func_name=None,
+ related_action_kwargs={},
+ )
+
+ def _parse_retry_pattern(self):
+ try:
+ # as json can't have integers as keys and the field is stored
+ # as json, convert back to int
+ retry_pattern = {
+ int(try_count): postpone_seconds
+ for try_count, postpone_seconds in self.retry_pattern.items()
+ }
+ except ValueError:
+ _logger.error(
+ "Invalid retry pattern for job function %s,"
+ " keys could not be parsed as integers, fallback"
+ " to the default retry pattern.",
+ self.name,
+ )
+ retry_pattern = {}
+ return retry_pattern
+
+ @tools.ormcache("name")
+ def job_config(self, name):
+ config = self.search([("name", "=", name)], limit=1)
+ if not config:
+ return self.job_default_config()
+ retry_pattern = config._parse_retry_pattern()
+ return self.JobConfig(
+ channel=config.channel,
+ retry_pattern=retry_pattern,
+ related_action_enable=config.related_action.get("enable", True),
+ related_action_func_name=config.related_action.get("func_name"),
+ related_action_kwargs=config.related_action.get("kwargs", {}),
+ )
+
+ def _retry_pattern_format_error_message(self):
+ return _(
+ "Unexpected format of Retry Pattern for {}.\n"
+ "Example of valid format:\n"
+ "{{1: 300, 5: 600, 10: 1200, 15: 3000}}"
+ ).format(self.name)
+
+ @api.constrains("retry_pattern")
+ def _check_retry_pattern(self):
+ for record in self:
+ retry_pattern = record.retry_pattern
+ if not retry_pattern:
+ continue
+
+ all_values = list(retry_pattern) + list(retry_pattern.values())
+ for value in all_values:
+ try:
+ int(value)
+ except ValueError:
+ raise exceptions.UserError(
+ record._retry_pattern_format_error_message()
+ )
+
+ def _related_action_format_error_message(self):
+ return _(
+ "Unexpected format of Related Action for {}.\n"
+ "Example of valid format:\n"
+ '{{"enable": True, "func_name": "related_action_foo",'
+ ' "kwargs" {{"limit": 10}}}}'
+ ).format(self.name)
+
+ @api.constrains("related_action")
+ def _check_related_action(self):
+ valid_keys = ("enable", "func_name", "kwargs")
+ for record in self:
+ related_action = record.related_action
+ if not related_action:
+ continue
+
+ if any(key not in valid_keys for key in related_action):
+ raise exceptions.UserError(
+ record._related_action_format_error_message()
+ )
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ records = self.browse()
+ if self.env.context.get("install_mode"):
+ # installing a module that creates a job function: rebinds the record
+ # to an existing one (likely we already had the job function created by
+ # the @job decorator previously)
+ new_vals_list = []
+ for vals in vals_list:
+ name = vals.get("name")
+ if name:
+ existing = self.search([("name", "=", name)], limit=1)
+ if existing:
+ if not existing.get_metadata()[0].get("noupdate"):
+ existing.write(vals)
+ records |= existing
+ continue
+ new_vals_list.append(vals)
+ vals_list = new_vals_list
+ records |= super().create(vals_list)
+ self.clear_caches()
+ return records
+
+ def write(self, values):
+ res = super().write(values)
+ self.clear_caches()
+ return res
+
+ def unlink(self):
+ res = super().unlink()
+ self.clear_caches()
+ return res
diff --git a/queue_job/readme/CONTRIBUTORS.rst b/queue_job/readme/CONTRIBUTORS.rst
index 9694c9654a..0f8bb1a3b2 100644
--- a/queue_job/readme/CONTRIBUTORS.rst
+++ b/queue_job/readme/CONTRIBUTORS.rst
@@ -8,3 +8,4 @@
* Cédric Pigeon
* Tatiana Deribina
* Souheil Bejaoui
+* Eric Antones
diff --git a/queue_job/readme/HISTORY.rst b/queue_job/readme/HISTORY.rst
index 072e87fd31..ef19313c45 100644
--- a/queue_job/readme/HISTORY.rst
+++ b/queue_job/readme/HISTORY.rst
@@ -15,24 +15,4 @@ Next
process (when running with --workers > 0)
* [REF] ``@job`` and ``@related_action`` deprecated, any method can be delayed,
and configured using ``queue.job.function`` records
-
-
-13.0.1.2.0 (2020-03-10)
-~~~~~~~~~~~~~~~~~~~~~~~
-
-* Fix Multi-company access rules
-
-
-13.0.1.1.0 (2019-11-01)
-~~~~~~~~~~~~~~~~~~~~~~~
-
-Important: the license has been changed from AGPL3 to LGPL3.
-
-* Remove deprecated default company method
- (details on `#180 `_)
-
-
-13.0.1.0.0 (2019-10-14)
-~~~~~~~~~~~~~~~~~~~~~~~
-
-* [MIGRATION] from 12.0 branched at rev. 0138cd0
+* [MIGRATION] from 13.0 branched at rev. e24ff4b
diff --git a/queue_job/security/ir.model.access.csv b/queue_job/security/ir.model.access.csv
index e90eee9ae4..9242305158 100644
--- a/queue_job/security/ir.model.access.csv
+++ b/queue_job/security/ir.model.access.csv
@@ -2,3 +2,5 @@ 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,1,1
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
diff --git a/queue_job/static/description/icon.svg b/queue_job/static/description/icon.svg
index a5fbb71bb6..fd5da3366b 100644
--- a/queue_job/static/description/icon.svg
+++ b/queue_job/static/description/icon.svg
@@ -1,138 +1,150 @@
-
+
-
diff --git a/queue_job/tests/common.py b/queue_job/tests/common.py
index 6795965b76..175e75a314 100644
--- a/queue_job/tests/common.py
+++ b/queue_job/tests/common.py
@@ -9,7 +9,7 @@
class JobCounter:
def __init__(self, env):
- super(JobCounter, self).__init__()
+ super().__init__()
self.env = env
self.existing = self.search_all()
diff --git a/queue_job/tests/test_model_job_channel.py b/queue_job/tests/test_model_job_channel.py
index 42ba7f3271..3fdc7b4c74 100644
--- a/queue_job/tests/test_model_job_channel.py
+++ b/queue_job/tests/test_model_job_channel.py
@@ -9,7 +9,7 @@
class TestJobChannel(common.TransactionCase):
def setUp(self):
- super(TestJobChannel, self).setUp()
+ super().setUp()
self.Channel = self.env["queue.job.channel"]
self.root_channel = self.Channel.search([("name", "=", "root")])
diff --git a/queue_job/views/queue_job_channel_views.xml b/queue_job/views/queue_job_channel_views.xml
new file mode 100644
index 0000000000..0841a2514e
--- /dev/null
+++ b/queue_job/views/queue_job_channel_views.xml
@@ -0,0 +1,58 @@
+
+
+
+
+ queue.job.channel.form
+ queue.job.channel
+
+
+
+
+
+
+ queue.job.channel.tree
+ queue.job.channel
+
+
+
+
+
+
+
+
+ queue.job.channel.search
+ queue.job.channel
+
+
+
+
+
+
+
+
+
+
+ Channels
+ queue.job.channel
+ tree,form
+ {}
+
+
+
+
diff --git a/queue_job/views/queue_job_function_views.xml b/queue_job/views/queue_job_function_views.xml
new file mode 100644
index 0000000000..a6e2ce402c
--- /dev/null
+++ b/queue_job/views/queue_job_function_views.xml
@@ -0,0 +1,58 @@
+
+
+
+
+ queue.job.function.form
+ queue.job.function
+
+
+
+
+
+
+ queue.job.function.tree
+ queue.job.function
+
+
+
+
+
+
+
+
+
+ queue.job.function.search
+ queue.job.function
+
+
+
+
+
+
+
+
+
+
+
+
+ Job Functions
+ queue.job.function
+ tree,form
+ {}
+
+
+
+
diff --git a/queue_job/views/queue_job_menus.xml b/queue_job/views/queue_job_menus.xml
new file mode 100644
index 0000000000..d288d7c0b9
--- /dev/null
+++ b/queue_job/views/queue_job_menus.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/queue_job/views/queue_job_views.xml b/queue_job/views/queue_job_views.xml
index 1197495ead..a68c03d34f 100644
--- a/queue_job/views/queue_job_views.xml
+++ b/queue_job/views/queue_job_views.xml
@@ -1,5 +1,6 @@
+
queue.job.form
queue.job
@@ -91,6 +92,7 @@
+
queue.job.tree
queue.job
@@ -113,6 +115,7 @@
+
queue.job.search
queue.job
@@ -169,6 +172,7 @@
+
Jobs
queue.job
@@ -180,185 +184,5 @@
-
- Requeue Jobs
- queue.requeue.job
-
-
-
-
-
- Set Jobs to Done
- queue.jobs.to.done
-
-
-
-
-
- Requeue Jobs
- queue.requeue.job
- form
-
- new
-
-
-
- Set jobs to done
- queue.jobs.to.done
- form
-
- new
-
-
-
- queue.job.channel.form
- queue.job.channel
-
-
-
-
-
- queue.job.channel.tree
- queue.job.channel
-
-
-
-
-
-
-
- queue.job.channel.search
- queue.job.channel
-
-
-
-
-
-
-
-
-
- Channels
- queue.job.channel
- tree,form
- {}
-
-
-
- queue.job.function.form
- queue.job.function
-
-
-
-
-
- queue.job.function.tree
- queue.job.function
-
-
-
-
-
-
-
-
- queue.job.function.search
- queue.job.function
-
-
-
-
-
-
-
-
-
-
-
- Job Functions
- queue.job.function
- tree,form
- {}
-
-
-
-
-
-
-
+
diff --git a/queue_job/wizards/__init__.py b/queue_job/wizards/__init__.py
new file mode 100644
index 0000000000..0794047e75
--- /dev/null
+++ b/queue_job/wizards/__init__.py
@@ -0,0 +1,2 @@
+from . import queue_requeue_job
+from . import queue_jobs_to_done
diff --git a/queue_job/wizards/queue_jobs_to_done.py b/queue_job/wizards/queue_jobs_to_done.py
new file mode 100644
index 0000000000..ff1366ffed
--- /dev/null
+++ b/queue_job/wizards/queue_jobs_to_done.py
@@ -0,0 +1,15 @@
+# Copyright 2013-2020 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo import models
+
+
+class SetJobsToDone(models.TransientModel):
+ _inherit = "queue.requeue.job"
+ _name = "queue.jobs.to.done"
+ _description = "Set all selected jobs to done"
+
+ def set_done(self):
+ jobs = self.job_ids
+ jobs.button_done()
+ return {"type": "ir.actions.act_window_close"}
diff --git a/queue_job/wizards/queue_jobs_to_done_views.xml b/queue_job/wizards/queue_jobs_to_done_views.xml
new file mode 100644
index 0000000000..fde48ab3dd
--- /dev/null
+++ b/queue_job/wizards/queue_jobs_to_done_views.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ Set Jobs to Done
+ queue.jobs.to.done
+
+
+
+
+
+
+ Set jobs to done
+ queue.jobs.to.done
+ form
+
+ new
+
+
+
+
diff --git a/queue_job/wizards/queue_requeue_job.py b/queue_job/wizards/queue_requeue_job.py
new file mode 100644
index 0000000000..67d2ffcbdc
--- /dev/null
+++ b/queue_job/wizards/queue_requeue_job.py
@@ -0,0 +1,25 @@
+# Copyright 2013-2020 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
+
+from odoo import fields, models
+
+
+class QueueRequeueJob(models.TransientModel):
+ _name = "queue.requeue.job"
+ _description = "Wizard to requeue a selection of jobs"
+
+ def _default_job_ids(self):
+ res = False
+ context = self.env.context
+ if context.get("active_model") == "queue.job" and context.get("active_ids"):
+ res = context["active_ids"]
+ return res
+
+ job_ids = fields.Many2many(
+ comodel_name="queue.job", string="Jobs", default=lambda r: r._default_job_ids()
+ )
+
+ def requeue(self):
+ jobs = self.job_ids
+ jobs.requeue()
+ return {"type": "ir.actions.act_window_close"}
diff --git a/queue_job/wizards/queue_requeue_job_views.xml b/queue_job/wizards/queue_requeue_job_views.xml
new file mode 100644
index 0000000000..9a4ca935e8
--- /dev/null
+++ b/queue_job/wizards/queue_requeue_job_views.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ Requeue Jobs
+ queue.requeue.job
+
+
+
+
+
+
+ Requeue Jobs
+ queue.requeue.job
+ form
+
+ new
+
+
+
+
diff --git a/queue_job_cron/__manifest__.py b/queue_job_cron/__manifest__.py
index 4379e830f6..24f7e3233f 100644
--- a/queue_job_cron/__manifest__.py
+++ b/queue_job_cron/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Scheduled Actions as Queue Jobs",
"version": "13.0.2.1.0",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
- "website": "https://github.com/OCA/queue/tree/12.0/queue_job_cron",
+ "website": "https://github.com/OCA/queue",
"license": "AGPL-3",
"category": "Generic Modules",
"depends": ["queue_job"],
diff --git a/queue_job_subscribe/__manifest__.py b/queue_job_subscribe/__manifest__.py
index 7223a50293..7b6453f193 100644
--- a/queue_job_subscribe/__manifest__.py
+++ b/queue_job_subscribe/__manifest__.py
@@ -4,7 +4,7 @@
"name": "Queue Job Subscribe",
"version": "13.0.1.0.0",
"author": "Acsone SA/NV, Odoo Community Association (OCA)",
- "website": "https://github.com/OCA/queue_job",
+ "website": "https://github.com/OCA/queue",
"summary": "Control which users are subscribed to queue job notifications",
"license": "AGPL-3",
"category": "Generic Modules",
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000..b4d39fb9e0
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+# generated from manifests external_dependencies
+requests
diff --git a/setup/queue_job/odoo/addons/queue_job b/setup/queue_job/odoo/addons/queue_job
new file mode 120000
index 0000000000..ac796aaa1c
--- /dev/null
+++ b/setup/queue_job/odoo/addons/queue_job
@@ -0,0 +1 @@
+../../../../queue_job
\ No newline at end of file
diff --git a/setup/queue_job/setup.py b/setup/queue_job/setup.py
new file mode 100644
index 0000000000..28c57bb640
--- /dev/null
+++ b/setup/queue_job/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/test_queue_job/odoo/addons/test_queue_job b/setup/test_queue_job/odoo/addons/test_queue_job
new file mode 120000
index 0000000000..0473d5583c
--- /dev/null
+++ b/setup/test_queue_job/odoo/addons/test_queue_job
@@ -0,0 +1 @@
+../../../../test_queue_job
\ No newline at end of file
diff --git a/setup/test_queue_job/setup.py b/setup/test_queue_job/setup.py
new file mode 100644
index 0000000000..28c57bb640
--- /dev/null
+++ b/setup/test_queue_job/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/test_queue_job/__manifest__.py b/test_queue_job/__manifest__.py
index a09c89b07f..6b4dfa89e5 100644
--- a/test_queue_job/__manifest__.py
+++ b/test_queue_job/__manifest__.py
@@ -3,7 +3,7 @@
{
"name": "Queue Job Tests",
- "version": "13.0.2.1.0",
+ "version": "14.0.1.0.0",
"author": "Camptocamp,Odoo Community Association (OCA)",
"license": "LGPL-3",
"category": "Generic Modules",
@@ -14,5 +14,5 @@
"data/queue_job_function_data.xml",
"security/ir.model.access.csv",
],
- "installable": False,
+ "installable": True,
}
diff --git a/test_queue_job/models/test_models.py b/test_queue_job/models/test_models.py
index 36fdb1c6f9..5a93fa0b4f 100644
--- a/test_queue_job/models/test_models.py
+++ b/test_queue_job/models/test_models.py
@@ -34,7 +34,7 @@ class TestQueueJob(models.Model):
name = fields.Char()
def testing_method(self, *args, **kwargs):
- """ Method used for tests
+ """Method used for tests
Return always the arguments and keyword arguments received
"""
@@ -76,17 +76,6 @@ def job_b(self):
def job_sub_channel(self):
return
- # TODO deprecated by :job-no-decorator:
- @property
- def dummy_property(self):
- """ Return foo
-
- Only there to check that properties are compatible
- with the automatic registration of job methods
- and their default channels.
- """
- return "foo"
-
class TestRelatedAction(models.Model):
diff --git a/test_queue_job/tests/common.py b/test_queue_job/tests/common.py
index 3a67dea4da..63ca8cca48 100644
--- a/test_queue_job/tests/common.py
+++ b/test_queue_job/tests/common.py
@@ -12,9 +12,6 @@ def setUp(self):
self.queue_job = self.env["queue.job"]
self.user = self.env["res.users"]
self.method = self.env["test.queue.job"].testing_method
- self.env["queue.job.function"]._register_job(
- self.env["test.queue.job"], self.method
- )
def _create_job(self):
test_job = Job(self.method)
diff --git a/test_queue_job/tests/test_job.py b/test_queue_job/tests/test_job.py
index f9b679cc27..0abe79f474 100644
--- a/test_queue_job/tests/test_job.py
+++ b/test_queue_job/tests/test_job.py
@@ -9,6 +9,7 @@
import odoo.tests.common as common
from odoo import SUPERUSER_ID
+from odoo.addons.queue_job import identity_exact
from odoo.addons.queue_job.exception import (
FailedJobError,
NoSuchJobError,
@@ -22,7 +23,6 @@
RETRY_INTERVAL,
STARTED,
Job,
- identity_exact,
)
from .common import JobCommonCase
@@ -327,7 +327,7 @@ class TestJobs(JobCommonCase):
""" Test jobs on other methods or with different job configuration """
def test_description(self):
- """ If no description is given to the job, it
+ """If no description is given to the job, it
should be computed from the function
"""
# if a docstring is defined for the function
@@ -610,8 +610,8 @@ def test_job_default_company_id(self):
)
def test_job_no_company_id(self):
- """ if we put an empty company_id in the context
- jobs are created without company_id
+ """if we put an empty company_id in the context
+ jobs are created without company_id
"""
env = self.env(context={"company_id": None})
stored = self._create_job(env)
diff --git a/test_queue_job/tests/test_job_channels.py b/test_queue_job/tests/test_job_channels.py
index 01173e0dd9..16c942603b 100644
--- a/test_queue_job/tests/test_job_channels.py
+++ b/test_queue_job/tests/test_job_channels.py
@@ -4,7 +4,7 @@
import odoo.tests.common as common
from odoo import exceptions
-from odoo.addons.queue_job.job import Job, job
+from odoo.addons.queue_job.job import Job
class TestJobChannels(common.TransactionCase):
@@ -30,9 +30,9 @@ def test_channel_tree(self):
self.channel_model.create({"name": "sub"})
def test_channel_root(self):
- with self.assertRaises(exceptions.Warning):
+ with self.assertRaises(exceptions.UserError):
self.root_channel.unlink()
- with self.assertRaises(exceptions.Warning):
+ with self.assertRaises(exceptions.UserError):
self.root_channel.name = "leaf"
def test_channel_on_job(self):
@@ -85,17 +85,6 @@ def test_set_channel_from_record(self):
self.assertEquals(channel.parent_id.parent_id.name, "root")
self.assertEquals(job_func.channel, "root.sub.subsub")
- # TODO deprecated by :job-no-decorator:
- def test_job_decorator(self):
- """ Test the job decorator """
- default_channel = "channel"
- retry_pattern = {1: 5}
- partial = job(
- None, default_channel=default_channel, retry_pattern=retry_pattern
- )
- self.assertEquals(partial.keywords.get("default_channel"), default_channel)
- self.assertEquals(partial.keywords.get("retry_pattern"), retry_pattern)
-
def test_default_removal_interval(self):
channel = self.channel_model.create(
{"name": "number", "parent_id": self.root_channel.id}