Browse Source
Merge pull request #1352 from simahawk/11-add-base_cron_oneshot
Merge pull request #1352 from simahawk/11-add-base_cron_oneshot
[11.0][ADD] base_cron_oneshotpull/1332/merge
beau sebastien
6 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 302 additions and 0 deletions
-
21base_cron_oneshot/README.rst
-
1base_cron_oneshot/__init__.py
-
17base_cron_oneshot/__manifest__.py
-
18base_cron_oneshot/data/ir_cron.xml
-
10base_cron_oneshot/data/ir_sequence.xml
-
1base_cron_oneshot/models/__init__.py
-
85base_cron_oneshot/models/ir_cron.py
-
2base_cron_oneshot/readme/CONTRIBUTORS.rst
-
8base_cron_oneshot/readme/DESCRIPTION.rst
-
4base_cron_oneshot/readme/HISTORY.rst
-
20base_cron_oneshot/readme/USAGE.rst
-
1base_cron_oneshot/tests/__init__.py
-
94base_cron_oneshot/tests/test_cron.py
-
20base_cron_oneshot/views/ir_cron.xml
@ -0,0 +1,21 @@ |
|||
**This file is going to be generated by oca-gen-addon-readme.** |
|||
|
|||
*Manual changes will be overwritten.* |
|||
|
|||
Please provide content in the ``readme`` directory: |
|||
|
|||
* **DESCRIPTION.rst** (required) |
|||
* INSTALL.rst (optional) |
|||
* CONFIGURE.rst (optional) |
|||
* **USAGE.rst** (optional, highly recommended) |
|||
* DEVELOP.rst (optional) |
|||
* ROADMAP.rst (optional) |
|||
* HISTORY.rst (optional, recommended) |
|||
* **CONTRIBUTORS.rst** (optional, highly recommended) |
|||
* CREDITS.rst (optional) |
|||
|
|||
Content of this README will also be drawn from the addon manifest, |
|||
from keys such as name, authors, maintainers, development_status, |
|||
and license. |
|||
|
|||
A good, one sentence summary in the manifest is also highly recommended. |
@ -0,0 +1 @@ |
|||
from . import models |
@ -0,0 +1,17 @@ |
|||
# Copyright (C) 2018 Camptocamp |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
{ |
|||
'name': """Oneshot cron""", |
|||
'summary': """Allows creating of single-use disposable crons.""", |
|||
'category': "Extra Tools", |
|||
'version': "11.0.1.0.0", |
|||
'author': "Camptocamp, " |
|||
"Odoo Community Association (OCA)", |
|||
'website': "https://github.com/OCA/server-tools", |
|||
'license': "AGPL-3", |
|||
'data': [ |
|||
'data/ir_sequence.xml', |
|||
'data/ir_cron.xml', |
|||
'views/ir_cron.xml', |
|||
], |
|||
} |
@ -0,0 +1,18 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<data noupdate="1"> |
|||
<record id="cron_oneshot_cleanup" model="ir.cron" forcecreate="True"> |
|||
<field name="name">Oneshot cron cleanup</field> |
|||
<field name="user_id" ref="base.user_root"/> |
|||
<field name="model_id" ref="model_ir_cron"/> |
|||
<field name="state">code</field> |
|||
<field name="code">model.cron_oneshot_cleanup()</field> |
|||
<field name="interval_number">1</field> |
|||
<field name="interval_type">days</field> |
|||
<!-- make it run the day after installation at midnight --> |
|||
<field name="nextcall" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')" /> |
|||
<field name="numbercall">-1</field> |
|||
<field name="doall" eval="False"/> |
|||
</record> |
|||
</data> |
|||
</odoo> |
@ -0,0 +1,10 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo noupdate="1"> |
|||
|
|||
<record id="seq_oneshot_name" model="ir.sequence"> |
|||
<field name="name">Oneshot cron names sequence</field> |
|||
<field name="code">cron.oneshot</field> |
|||
<field name="prefix">Oneshot#</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -0,0 +1 @@ |
|||
from . import ir_cron |
@ -0,0 +1,85 @@ |
|||
# Copyright (C) 2018 by Camptocamp |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
from odoo import api, fields, models, _ |
|||
from datetime import datetime, timedelta |
|||
import logging |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class IrCron(models.Model): |
|||
_inherit = 'ir.cron' |
|||
|
|||
oneshot = fields.Boolean( |
|||
string='Single use', |
|||
default=False, |
|||
help='Enable this to run the cron only once. ' |
|||
'The cron will be deleted right after execution.' |
|||
) |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
if vals.get('oneshot'): |
|||
vals.update(self._oneshot_defaults(**vals)) |
|||
return super().create(vals) |
|||
|
|||
def _oneshot_defaults( |
|||
self, name=None, delay=('minutes', 10), nextcall=None, **kw): |
|||
if nextcall is None: |
|||
nextcall = fields.Datetime.to_string( |
|||
datetime.now() + timedelta(**dict([delay, ])) |
|||
) |
|||
return { |
|||
'state': 'code', |
|||
# TODO: shall we enforce `doall` too? |
|||
# enforce numbercall |
|||
'numbercall': 1, |
|||
# make sure name is automatic |
|||
'name': self._oneshot_make_name(name), |
|||
'nextcall': nextcall, |
|||
} |
|||
|
|||
def _oneshot_make_name(self, name=None): |
|||
name = ' ' + (name if name else '') |
|||
return '{}{}'.format( |
|||
self.env['ir.sequence'].next_by_code('cron.oneshot'), name |
|||
) |
|||
|
|||
@api.model |
|||
def schedule_oneshot(self, model_name, method=None, code=None, |
|||
delay=('minutes', 10), **kw): |
|||
"""Create a one shot cron. |
|||
|
|||
:param model_name: a string matching an odoo model name |
|||
:param method: an existing method to call on the model |
|||
:param code: custom code to run on the model |
|||
:param delay: timedelta compat values for delay as tuple |
|||
:param kw: custom values for the cron |
|||
""" |
|||
assert method or code, _('Provide a method or some code!') |
|||
if method and not code: |
|||
code = 'model.{}()'.format(method) |
|||
model = self.env['ir.model']._get(model_name) |
|||
vals = { |
|||
'model_id': model.id, |
|||
'code': code, |
|||
} |
|||
vals.update(self._oneshot_defaults(delay=delay)) |
|||
vals.update(kw) |
|||
# make sure is a oneshot cron ;) |
|||
vals['oneshot'] = True |
|||
return self.create(vals) |
|||
|
|||
def _oneshot_cleanup_domain(self): |
|||
# TODO: any better way to select them? |
|||
return [ |
|||
('oneshot', '=', True), |
|||
('numbercall', '=', 0), # already executed |
|||
('active', '=', False), # already executed and numbercall=0 |
|||
] |
|||
|
|||
@api.model |
|||
def cron_oneshot_cleanup(self): |
|||
self.with_context( |
|||
active_test=False |
|||
).search(self._oneshot_cleanup_domain()).unlink() |
@ -0,0 +1,2 @@ |
|||
* Simone Orsi <simone.orsi@camptocamp.com> |
|||
* Artem Kostyuk <a.kostyuk@mobilunity.com> |
@ -0,0 +1,8 @@ |
|||
This module extends the functionality of Odoo crons |
|||
to allow you to create single-purpose crons without any further setup or modules |
|||
such as `queue_job`. |
|||
|
|||
The typical use case is: you have an expensive task to run on demand and only once. |
|||
|
|||
A main cron called "Oneshot cron cleanup" will delete already executed crons each day. |
|||
You might want to tune it according to your needs. |
@ -0,0 +1,4 @@ |
|||
11.0.1.0.0 (2018-08-30) |
|||
~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
* First release |
@ -0,0 +1,20 @@ |
|||
You can create crons as usual via the admin interface or via code. |
|||
The important thing, in both case, is to set `oneshot` flag as true. |
|||
|
|||
Developer shortcut |
|||
------------------ |
|||
|
|||
You can easily create a oneshot cron like this: |
|||
|
|||
.. code-block:: python |
|||
|
|||
cron = self.env['ir.cron'].schedule_oneshot( |
|||
'res.partner', method='my_cron_method') |
|||
|
|||
If you need to customize other parameters you can pass them as keyword args: |
|||
|
|||
.. code-block:: python |
|||
|
|||
my_values = {...} |
|||
cron = self.env['ir.cron'].schedule_oneshot( |
|||
'res.partner', method='my_cron_method', **my_values) |
@ -0,0 +1 @@ |
|||
from . import test_cron |
@ -0,0 +1,94 @@ |
|||
# Copyright (C) 2018 Camptocamp |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
# pylint: disable=anomalous-backslash-in-string |
|||
|
|||
from odoo import api |
|||
from odoo.tests import common |
|||
|
|||
import datetime |
|||
import mock |
|||
|
|||
|
|||
MOCK_PATH = 'odoo.addons.base_cron_oneshot.models.ir_cron' |
|||
|
|||
|
|||
class OneshotTestCase(common.SavepointCase): |
|||
|
|||
@property |
|||
def cron_model(self): |
|||
return self.env['ir.cron'] |
|||
|
|||
@mock.patch(MOCK_PATH + '.datetime') |
|||
def test_defaults(self, mocked_dt): |
|||
mocked_dt.now.return_value = datetime.datetime(2018, 8, 31, 10, 30) |
|||
cron = self.cron_model.create({ |
|||
'oneshot': True, |
|||
'name': 'Foo', |
|||
'model_id': self.env['ir.model']._get('ir.cron').id, |
|||
'state': 'code', |
|||
'code': 'model.some_method()', |
|||
'interval_number': 1, |
|||
'interval_type': 'days', |
|||
'numbercall': 5, # won't have any effect |
|||
}) |
|||
self.assertRegexpMatches(cron.name, 'Oneshot#\d+ Foo') |
|||
self.assertEqual(cron.numbercall, 1) |
|||
# call postponed by 10mins |
|||
self.assertEqual(cron.nextcall, '2018-08-31 10:40:00') |
|||
|
|||
def test_schedule_oneshot_check(self): |
|||
with self.assertRaises(AssertionError) as err: |
|||
self.cron_model.schedule_oneshot('res.partner') |
|||
self.assertEqual(str(err.exception), 'Provide a method or some code!') |
|||
|
|||
@mock.patch(MOCK_PATH + '.datetime') |
|||
def test_schedule_oneshot_method(self, mocked_dt): |
|||
mocked_dt.now.return_value = datetime.datetime(2018, 8, 31, 16, 30) |
|||
cron = self.cron_model.schedule_oneshot( |
|||
'res.partner', method='read', delay=('minutes', 30)) |
|||
self.assertRegexpMatches(cron.name, 'Oneshot#\d+') |
|||
self.assertEqual(cron.numbercall, 1) |
|||
self.assertEqual(cron.code, 'model.read()') |
|||
self.assertEqual( |
|||
cron.model_id, self.env['ir.model']._get('res.partner')) |
|||
self.assertEqual(cron.nextcall, '2018-08-31 17:00:00') |
|||
|
|||
def test_schedule_oneshot_code(self): |
|||
cron = self.cron_model.schedule_oneshot( |
|||
'res.partner', code='env["res.partner"].search([])') |
|||
self.assertRegexpMatches(cron.name, 'Oneshot#\d+') |
|||
self.assertEqual(cron.numbercall, 1) |
|||
self.assertEqual(cron.state, 'code') |
|||
self.assertEqual(cron.code, 'env["res.partner"].search([])') |
|||
self.assertEqual( |
|||
cron.model_id, self.env['ir.model']._get('res.partner')) |
|||
|
|||
|
|||
class OneshotProcessTestCase(common.TransactionCase): |
|||
|
|||
def setUp(self): |
|||
super().setUp() |
|||
deleted = [] |
|||
|
|||
@api.multi |
|||
def unlink(self): |
|||
deleted.extend(self.ids) |
|||
# do nothing as the original one will try to read the lock |
|||
# for the current record which is NOT committed |
|||
# and has no real ID. |
|||
return |
|||
|
|||
self.env['ir.cron']._patch_method('unlink', unlink) |
|||
self.addCleanup(self.env['ir.cron']._revert_method, 'unlink') |
|||
self.deleted = deleted |
|||
|
|||
def test_schedule_oneshot_cleanup(self): |
|||
cron1 = self.env['ir.cron'].schedule_oneshot( |
|||
'res.partner', code='env["res.partner"].search([])') |
|||
cron2 = self.env['ir.cron'].schedule_oneshot( |
|||
'res.partner', code='env["res.partner"].read([])') |
|||
# simulate excuted |
|||
cron1.write({'numbercall': 0, 'active': False}) |
|||
self.env['ir.cron'].cron_oneshot_cleanup() |
|||
self.assertIn(cron1.id, self.deleted) |
|||
self.assertNotIn(cron2.id, self.deleted) |
@ -0,0 +1,20 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
|
|||
<record id="ir_cron_view_form" model="ir.ui.view"> |
|||
<field name="name">ir.cron.form</field> |
|||
<field name="model">ir.cron</field> |
|||
<field name="inherit_id" ref="base.ir_cron_view_form"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="state" position="after"> |
|||
<field name="oneshot"/> |
|||
</field> |
|||
<field name="numbercall" position="attributes"> |
|||
<attribute name="attrs"> |
|||
{'readonly': [('oneshot', '=', True)]} |
|||
</attribute> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
|
|||
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue