[16.0][MIG][REF] attachment_queue: Migration to 16.0

pull/2560/head
Kevin Khao 2023-02-22 13:12:28 +02:00 committed by Florian da Costa
parent b7bd5a8323
commit 178f40731a
9 changed files with 208 additions and 105 deletions

View File

@ -3,21 +3,19 @@
{
"name": "Attachment Queue",
"version": "14.0.1.0.1",
"version": "16.0.1.0.1",
"author": "Akretion,Odoo Community Association (OCA)",
"summary": "Base module adding the concept of queue for processing files",
"website": "https://github.com/OCA/server-tools",
"maintainers": ["florian-dacosta", "sebastienbeau"],
"license": "AGPL-3",
"category": "Generic Modules",
"depends": ["base", "mail"],
"depends": ["base", "mail", "queue_job"],
"data": [
"views/attachment_queue_view.xml",
"security/ir.model.access.csv",
"data/cron.xml",
"data/ir_config_parameter.xml",
"data/mail_template.xml",
"data/queue_job_channel.xml",
],
"demo": ["demo/attachment_queue_demo.xml"],
"installable": True,
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo noupdate="1">
<record model="ir.cron" id="cronjob_run_attachment_queue">
<field name='name'>Run Attachments Queue</field>
<field name='interval_number'>30</field>
<field name='interval_type'>minutes</field>
<field name="numbercall">-1</field>
<field name="active">False</field>
<field name="doall" eval="False" />
<field name="model_id" ref="model_attachment_queue" />
<field name="state">code</field>
<field name="code">model.run_attachment_queue_scheduler()</field>
</record>
</odoo>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="attachment_queue_cron_batch_limit" model="ir.config_parameter">
<field name="key">attachment_queue_cron_batch_limit</field>
<field name="value">200</field>
</record>
</odoo>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="attachment_queue_job_channel" model="queue.job.channel">
<field name="name">Attachment queues</field>
<field name="parent_id" ref="queue_job.channel_root" />
</record>
</odoo>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" ?>
<odoo noupdate="1">
<record id="attachment_queue_demo" model="attachment.queue">
<field name="datas">bWlncmF0aW9uIHRlc3Q=</field>
<field name="name">attachment_queue_demo.doc</field>
</record>
</odoo>

View File

@ -2,10 +2,19 @@
import logging
from odoo import SUPERUSER_ID, api, fields, models, registry
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.addons.queue_job.exception import RetryableJobError
_logger = logging.getLogger(__name__)
DEFAULT_ETA_FOR_RETRY = 60 * 60
STR_ERR_ATTACHMENT_RUNNING = (
"The attachment is currently flagged as being in processing"
)
STR_ERROR_DURING_PROCESSING = "Error during processing of attachment_queue id {}: \n"
class AttachmentQueue(models.Model):
_name = "attachment.queue"
@ -34,10 +43,38 @@ class AttachmentQueue(models.Model):
state_message = fields.Text()
failure_emails = fields.Char(
compute="_compute_failure_emails",
string="Failure Emails",
help="Comma-separated list of email addresses to be notified in case of"
"failure",
)
running_lock = fields.Boolean()
@property
def _eta_for_retry(self):
return DEFAULT_ETA_FOR_RETRY
@property
def _job_attrs(self):
return {"channel": "Attachment queues"}
def _schedule_jobs(self):
for el in self:
el.with_delay(**self._job_attrs).run()
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res._schedule_jobs()
return res
def button_reschedule(self):
self.state = "pending"
self.state_message = ""
self._schedule_jobs()
def button_manual_run(self):
if self.running_lock:
raise UserError(STR_ERR_ATTACHMENT_RUNNING)
self.run()
def _compute_failure_emails(self):
for attach in self:
@ -48,51 +85,41 @@ class AttachmentQueue(models.Model):
self.ensure_one()
return ""
@api.model
def run_attachment_queue_scheduler(self, domain=None):
if domain is None:
domain = [("state", "=", "pending")]
batch_limit = self.env.ref(
"attachment_queue.attachment_queue_cron_batch_limit"
).value
if batch_limit and batch_limit.isdigit():
limit = int(batch_limit)
else:
limit = 200
attachments = self.search(domain, limit=limit)
if attachments:
return attachments.run()
return True
def run(self):
"""
Run the process for each attachment queue
Run the process for an individual attachment queue
"""
failure_tmpl = self.env.ref("attachment_queue.attachment_failure_notification")
for attachment in self:
with api.Environment.manage():
with registry(self.env.cr.dbname).cursor() as new_cr:
new_env = api.Environment(new_cr, SUPERUSER_ID, self.env.context)
attach = attachment.with_env(new_env)
try:
attach._run()
# pylint: disable=broad-except
except Exception as e:
attach.env.cr.rollback()
_logger.exception(str(e))
attach.write({"state": "failed", "state_message": str(e)})
emails = attach.failure_emails
if emails:
failure_tmpl.send_mail(attach.id)
attach.env.cr.commit()
else:
vals = {
"state": "done",
"date_done": fields.Datetime.now(),
}
attach.write(vals)
attach.env.cr.commit()
return True
if self.state != "pending":
return
if self.running_lock is True:
raise RetryableJobError(
STR_ERR_ATTACHMENT_RUNNING, seconds=self._eta_for_retry
)
self.running_lock = True
self.flush_recordset()
try:
with self.env.cr.savepoint():
self._run()
except Exception as e:
_logger.warning(STR_ERROR_DURING_PROCESSING.format(self.id) + str(e))
self.write(
{"state": "failed", "state_message": str(e), "running_lock": False}
)
emails = self.failure_emails
if emails:
self.env.ref(
"attachment_queue.attachment_failure_notification"
).send_mail(self.id)
return False
else:
self.write(
{
"state": "done",
"date_done": fields.Datetime.now(),
"running_lock": False,
}
)
return True
def _run(self):
self.ensure_one()

View File

@ -1,35 +1,111 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import odoo
from odoo import api
from unittest import mock
from odoo_test_helper import FakeModelLoader
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
from odoo.addons.queue_job.exception import RetryableJobError
from odoo.addons.queue_job.tests.common import trap_jobs
DUMMY_AQ_VALS = {
"datas": "",
"name": "dummy_aq.doc",
}
MOCK_PATH_RUN = (
"odoo.addons.attachment_queue.models.attachment_queue.AttachmentQueue._run"
)
class TestAttachmentBaseQueue(TransactionCase):
def _create_dummy_attachment(self, override=False, no_job=False):
override = override or {}
vals = DUMMY_AQ_VALS.copy()
vals.update(override)
if no_job:
return (
self.env["attachment.queue"].with_context(test_queue_job_no_delay=True)
).create(vals)
return self.env["attachment.queue"].create(vals)
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.loader = FakeModelLoader(cls.env, cls.__module__)
cls.loader.backup_registry()
from .test_models import AttachmentQueue
cls.loader.update_registry((AttachmentQueue,))
@classmethod
def tearDownClass(cls):
super().tearDownClass()
cls.loader.restore_registry()
return super().tearDownClass()
def setUp(self):
super().setUp()
self.registry.enter_test_mode(self.env.cr)
self.env = api.Environment(
self.registry.test_cr, self.env.uid, self.env.context
)
self.attachment = self.env.ref("attachment_queue.attachment_queue_demo")
self.aq_model = self.env["attachment.queue"]
def tearDown(self):
self.registry.leave_test_mode()
super().tearDown()
def test_job_created(self):
with trap_jobs() as trap:
self._create_dummy_attachment()
trap.assert_enqueued_job(
self.env["attachment.queue"].run,
)
def test_attachment_queue(self):
"""Test run_attachment_queue_scheduler to ensure set state to done"""
self.assertEqual(self.attachment.state, "pending")
self.env["attachment.queue"].run_attachment_queue_scheduler()
self.env.cache.invalidate()
with odoo.registry(self.env.cr.dbname).cursor() as new_cr:
new_env = api.Environment(new_cr, self.env.uid, self.env.context)
attach = self.attachment.with_env(new_env)
self.assertEqual(attach.state, "done")
def test_aq_locked_job(self):
"""If an attachment is already running, and a job tries to run it, retry later"""
with self.assertRaises(RetryableJobError):
self._create_dummy_attachment({"running_lock": True}, no_job=True)
def test_aq_locked_button(self):
"""If an attachment is already running, and a user tries to run it manually,
raise error window"""
attachment = self._create_dummy_attachment(no_job=True)
attachment.running_lock = True
with self.assertRaises(UserError):
attachment.button_manual_run()
def test_run_ok(self):
"""Attachment queue should have correct state and result"""
partners_initial = len(self.env["res.partner"].search([]))
with mock.patch.object(
type(self.aq_model),
"_run",
self.env["attachment.queue"].mock_run_create_partners,
):
attachment = self._create_dummy_attachment(no_job=True)
partners_after = len(self.env["res.partner"].search([]))
self.assertEqual(partners_after, partners_initial + 10)
self.assertEqual(attachment.state, "done")
def test_run_fails(self):
"""Attachment queue should have correct state/error message"""
with mock.patch.object(
type(self.aq_model), "_run", self.env["attachment.queue"].mock_run_fail
):
attachment = self._create_dummy_attachment(no_job=True)
self.assertEqual(attachment.state, "failed")
self.assertEqual(attachment.state_message, "boom")
def test_run_fails_rollback(self):
"""In case of failure, no side effects should occur"""
partners_initial = len(self.env["res.partner"].search([]))
with mock.patch.object(
type(self.aq_model),
"_run",
self.env["attachment.queue"].mock_run_create_partners_and_fail,
):
self._create_dummy_attachment(no_job=True)
partners_after = len(self.env["res.partner"].search([]))
self.assertEqual(partners_after, partners_initial)
def test_set_done(self):
"""Test set_done manually"""
self.assertEqual(self.attachment.state, "pending")
self.attachment.set_done()
self.assertEqual(self.attachment.state, "done")
attachment = self._create_dummy_attachment()
self.assertEqual(attachment.state, "pending")
attachment.set_done()
self.assertEqual(attachment.state, "done")

View File

@ -0,0 +1,20 @@
# Copyright 2023 Akretion
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, models
from odoo.exceptions import UserError
class AttachmentQueue(models.Model):
_inherit = "attachment.queue"
_name = "attachment.queue"
def mock_run_fail(self):
raise UserError(_("boom"))
def mock_run_create_partners(self):
for x in range(10):
self.env["res.partner"].create({"name": str(x)})
def mock_run_create_partners_and_fail(self):
self.mock_run_create_partners()
raise UserError(_("boom"))

View File

@ -9,9 +9,16 @@
<xpath expr="/form/*" position="before">
<header>
<button
name="run"
name="button_manual_run"
states="pending,failed"
string="Run"
string="Manual run"
type="object"
class="oe_highlight"
/>
<button
name="button_reschedule"
states="done,failed"
string="Reschedule"
type="object"
class="oe_highlight"
/>