changeset 32265209fd9e in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=32265209fd9e
description:
        Simplify cron

        issue8201
        issue6117
        issue2286
diffstat:

 CHANGELOG                     |    1 +
 doc/topics/cron.rst           |   47 ++++++++++
 doc/topics/index.rst          |    1 +
 doc/topics/start_server.rst   |    4 +-
 trytond/ir/cron.py            |  182 +++++++++++++----------------------------
 trytond/ir/view/cron_form.xml |   40 ++++-----
 trytond/ir/view/cron_list.xml |    5 +-
 trytond/res/ir.xml            |   19 +----
 8 files changed, 129 insertions(+), 170 deletions(-)

diffs (432 lines):

diff -r 072daf920a7c -r 32265209fd9e CHANGELOG
--- a/CHANGELOG Fri Apr 12 13:08:01 2019 +0200
+++ b/CHANGELOG Fri Apr 12 15:59:25 2019 +0200
@@ -1,3 +1,4 @@
+* Simplify cron
 * Add duration on Cache
 * Add strip wildcards helpers
 * Add list-form view
diff -r 072daf920a7c -r 32265209fd9e doc/topics/cron.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/topics/cron.rst       Fri Apr 12 15:59:25 2019 +0200
@@ -0,0 +1,47 @@
+.. _topics-cron:
+
+=================
+Scheduled Actions
+=================
+
+Tryton provides a scheduler (aka cron) which can execute methods of
+:ref:`models <topics-models>` periodically at set intervals.
+
+The planning is managed by ``ir.cron`` records which store the method to call
+and the interval of time between calls. The method must be a class method of a
+:class:`~trytond.model.Model` which can be called without any parameters.
+
+To register a new method with the scheduler, you must extend the ``ir.cron``
+model and append the new method to the
+:attr:`~trytond.model.fields.Selection.selection` attribute of the ``method``
+field in :meth:`~trytond.model.Model.__setup__()`. The name of the selection
+must be the model name and the method name joined together with a ``|`` between
+them.
+
+Example:
+
+.. highlight:: python
+
+::
+
+    from trytond.model import Model
+    from trytond.pool import PoolMeta
+
+    class Cron(metaclass=PoolMeta):
+        __name__ = 'ir.cron'
+
+        @classmethod
+        def __setup__(cls):
+            super().__setup__()
+            cls.method.selection.append(
+                ('my_model|my_method', "Run my method"),
+                )
+
+
+    class MyModel(Model):
+        "My Model"
+        __name__ = 'my_model'
+
+        @classmethod
+        def my_method(cls):
+            pass
diff -r 072daf920a7c -r 32265209fd9e doc/topics/index.rst
--- a/doc/topics/index.rst      Fri Apr 12 13:08:01 2019 +0200
+++ b/doc/topics/index.rst      Fri Apr 12 15:59:25 2019 +0200
@@ -30,6 +30,7 @@
     reports/index
     rpc
     task_queue
+    cron
     user_application
     bus
     modules/index
diff -r 072daf920a7c -r 32265209fd9e doc/topics/start_server.rst
--- a/doc/topics/start_server.rst       Fri Apr 12 13:08:01 2019 +0200
+++ b/doc/topics/start_server.rst       Fri Apr 12 15:59:25 2019 +0200
@@ -43,8 +43,8 @@
 Cron service
 ============
 
-If you want to run some scheduled actions, you must also run the cron server
-with this command line::
+If you want to run some :ref:`scheduled actions <topics-cron>`, you must also
+run the cron server with this command line::
 
     trytond-cron -c <config file> -d <database>
 
diff -r 072daf920a7c -r 32265209fd9e trytond/ir/cron.py
--- a/trytond/ir/cron.py        Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/ir/cron.py        Fri Apr 12 15:59:25 2019 +0200
@@ -2,46 +2,20 @@
 # this repository contains the full copyright notices and license terms.
 import datetime
 from dateutil.relativedelta import relativedelta
-import traceback
-import sys
 import logging
-from email.mime.text import MIMEText
-from email.header import Header
-from ast import literal_eval
 
-from trytond.i18n import gettext
 from ..model import ModelView, ModelSQL, DeactivableMixin, fields, dualmethod
 from ..transaction import Transaction
 from ..pool import Pool
-from ..config import config
-from ..sendmail import sendmail
+from trytond.pyson import Eval
 from trytond.worker import run_task
 
-__all__ = [
-    'Cron',
-    ]
-
 logger = logging.getLogger(__name__)
 
-_INTERVALTYPES = {
-    'days': lambda interval: relativedelta(days=interval),
-    'hours': lambda interval: relativedelta(hours=interval),
-    'weeks': lambda interval: relativedelta(weeks=interval),
-    'months': lambda interval: relativedelta(months=interval),
-    'minutes': lambda interval: relativedelta(minutes=interval),
-}
-
 
 class Cron(DeactivableMixin, ModelSQL, ModelView):
     "Cron"
     __name__ = "ir.cron"
-    name = fields.Char('Name', required=True, translate=True)
-    user = fields.Many2One('res.user', 'Execution User', required=True,
-        domain=[('active', '=', False)],
-        help="The user used to execute this action")
-    request_user = fields.Many2One(
-        'res.user', 'Request User', required=True,
-        help="The user who will receive requests in case of failure")
     interval_number = fields.Integer('Interval Number', required=True)
     interval_type = fields.Selection([
             ('minutes', 'Minutes'),
@@ -49,17 +23,35 @@
             ('days', 'Days'),
             ('weeks', 'Weeks'),
             ('months', 'Months'),
-            ], 'Interval Unit')
-    number_calls = fields.Integer('Number of Calls', select=1, required=True,
-       help=('Number of times the function is called, a negative '
-           'number indicates that the function will always be '
-           'called'))
-    repeat_missed = fields.Boolean('Repeat Missed')
-    next_call = fields.DateTime('Next Call', required=True,
-            select=True)
-    model = fields.Char('Model')
-    function = fields.Char('Function')
-    args = fields.Text('Arguments')
+            ], "Interval Type", required=True)
+    minute = fields.Integer("Minute",
+        states={
+            'invisible': Eval('interval_type').in_(['minutes']),
+            },
+        depends=['interval_type'])
+    hour = fields.Integer("Hour",
+        states={
+            'invisible': Eval('interval_type').in_(['minutes', 'hours']),
+            },
+        depends=['interval_type'])
+    weekday = fields.Many2One(
+        'ir.calendar.day', "Day of Week",
+        states={
+            'invisible': Eval('interval_type').in_(
+                ['minutes', 'hours', 'days']),
+            },
+        depends=['interval_type'])
+    day = fields.Integer("Day",
+        states={
+            'invisible': Eval('interval_type').in_(
+                ['minutes', 'hours', 'days', 'weeks']),
+            },
+        depends=['interval_type'])
+
+    next_call = fields.DateTime("Next Call", select=True)
+    method = fields.Selection([
+            ('ir.trigger|trigger_time', "Run On Time Triggers"),
+            ], "Method", required=True)
 
     @classmethod
     def __setup__(cls):
@@ -70,84 +62,43 @@
                     },
                 })
 
-    @staticmethod
-    def default_next_call():
-        return datetime.datetime.now()
+    @classmethod
+    def __register__(cls, module_name):
+        super().__register__(module_name)
 
-    @staticmethod
-    def default_interval_number():
-        return 1
+        table_h = cls.__table_handler__(module_name)
 
-    @staticmethod
-    def default_interval_type():
-        return 'months'
+        # Migration from 5.0: remove fields
+        for column in ['name', 'user', 'request_user', 'number_calls',
+                'repeat_missed', 'model', 'function', 'args']:
+            table_h.drop_column(column)
 
-    @staticmethod
-    def default_number_calls():
-        return -1
-
-    @staticmethod
-    def default_repeat_missed():
-        return True
+        # Migration from 5.0: remove required on next_call
+        table_h.not_null_action('next_call', 'remove')
 
     @staticmethod
     def check_xml_record(crons, values):
         return True
 
-    @staticmethod
-    def get_delta(cron):
-        '''
-        Return the relativedelta for the next call
-        '''
-        return _INTERVALTYPES[cron.interval_type](cron.interval_number)
-
-    def send_error_message(self):
-        pool = Pool()
-        Config = pool.get('ir.configuration')
-
-        if self.request_user.language:
-            language = self.request_user.language.code
-        else:
-            language = Config.get_language()
-
-        with Transaction().set_user(self.user.id), \
-                Transaction().set_context(language=language):
-            tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
-            # On Python3, the traceback is already a unicode
-            if hasattr(tb_s, 'decode'):
-                tb_s = tb_s.decode('utf-8', 'ignore')
-            message = gettext(
-                'ir.msg_cron_email',
-                name=self.name,
-                url=self.__url__,
-                traceback=tb_s).splitlines()
-            subject = message[0]
-            body = '\n'.join(message[1:])
-
-            from_addr = config.get('email', 'from')
-            to_addr = self.request_user.email
-
-            msg = MIMEText(body, _charset='utf-8')
-            msg['To'] = to_addr
-            msg['From'] = from_addr
-            msg['Subject'] = Header(subject, 'utf-8')
-            if not to_addr:
-                logger.error(msg.as_string())
-            else:
-                sendmail(from_addr, to_addr, msg)
+    def compute_next_call(self, now):
+        return (now
+            + relativedelta(**{self.interval_type: self.interval_number})
+            + relativedelta(
+                microsecond=0,
+                second=0,
+                minute=self.minute,
+                hour=self.hour,
+                day=self.day,
+                weekday=int(self.weekday.index) if self.weekday else None))
 
     @dualmethod
     @ModelView.button
     def run_once(cls, crons):
         pool = Pool()
         for cron in crons:
-            if cron.args:
-                args = literal_eval(cron.args)
-            else:
-                args = []
-            Model = pool.get(cron.model)
-            with Transaction().set_user(cron.user.id):
-                getattr(Model, cron.function)(*args)
+            model, method = cron.method.split('|')
+            Model = pool.get(model)
+            getattr(Model, method)()
 
     @classmethod
     def run(cls, db_name):
@@ -155,33 +106,16 @@
         now = datetime.datetime.now()
         with Transaction().start(db_name, 0) as transaction:
             transaction.database.lock(transaction.connection, cls._table)
-            crons = cls.search([
-                    ('number_calls', '!=', 0),
-                    ('next_call', '<=', datetime.datetime.now()),
+            crons = cls.search(['OR',
+                    ('next_call', '<=', now),
+                    ('next_call', '=', None),
                     ])
 
             for cron in crons:
                 logger.info("Run cron %s", cron.id)
                 try:
-                    next_call = cron.next_call
-                    number_calls = cron.number_calls
-                    first = True
-                    while next_call < now and number_calls != 0:
-                        if first or cron.repeat_missed:
-                            try:
-                                cron.run_once()
-                            except Exception:
-                                transaction.rollback()
-                                cron.send_error_message()
-                        next_call += cls.get_delta(cron)
-                        if number_calls > 0:
-                            number_calls -= 1
-                        first = False
-
-                    cron.next_call = next_call
-                    cron.number_calls = number_calls
-                    if not number_calls:
-                        cron.active = False
+                    cron.run_once()
+                    cron.next_call = cron.compute_next_call(now)
                     cron.save()
                     transaction.commit()
                 except Exception:
diff -r 072daf920a7c -r 32265209fd9e trytond/ir/view/cron_form.xml
--- a/trytond/ir/view/cron_form.xml     Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/ir/view/cron_form.xml     Fri Apr 12 15:59:25 2019 +0200
@@ -2,31 +2,27 @@
 <!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
 this repository contains the full copyright notices and license terms. -->
 <form>
-    <label name="name"/>
-    <field name="name"/>
+    <label name="method"/>
+    <field name="method"/>
     <label name="active"/>
     <field name="active"/>
-    <label name="user"/>
-    <field name="user"/>
-    <label name="request_user"/>
-    <field name="request_user"/>
-    <label name="interval_number"/>
-    <field name="interval_number"/>
-    <label name="interval_type"/>
-    <field name="interval_type"/>
-    <label name="number_calls"/>
-    <field name="number_calls"/>
+    <label id="interval_label" string="Every"/>
+    <group id="interval" col="-1" colspan="3">
+        <field name="interval_number"/>
+        <field name="interval_type"/>
+    </group>
+    <label id="time_label" string="At"/>
+    <group id="time" col="-1" colspan="3">
+        <label name="minute"/>
+        <field name="minute" xexpand="0"/>
+        <label name="hour"/>
+        <field name="hour" xexpand="0"/>
+        <label name="day"/>
+        <field name="day" xexpand="0"/>
+        <label name="weekday"/>
+        <field name="weekday" xexpand="0" widget="selection"/>
+    </group>
     <label name="next_call"/>
     <field name="next_call"/>
-    <label name="repeat_missed"/>
-    <field name="repeat_missed"/>
     <button name="run_once" colspan="2"/>
-    <separator string="Action to trigger" colspan="4"
-        id="action_trigger"/>
-    <label name="model"/>
-    <field name="model"/>
-    <label name="function"/>
-    <field name="function"/>
-    <label name="args"/>
-    <field name="args" colspan="3" widget="char"/>
 </form>
diff -r 072daf920a7c -r 32265209fd9e trytond/ir/view/cron_list.xml
--- a/trytond/ir/view/cron_list.xml     Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/ir/view/cron_list.xml     Fri Apr 12 15:59:25 2019 +0200
@@ -2,13 +2,10 @@
 <!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
 this repository contains the full copyright notices and license terms. -->
 <tree>
+    <field name="method"/>
     <field name="next_call" widget="date"/>
     <field name="next_call" widget="time"/>
-    <field name="name"/>
-    <field name="user"/>
-    <field name="request_user"/>
     <field name="interval_number"/>
     <field name="interval_type"/>
-    <field name="number_calls"/>
     <button name="run_once" string="Run Once" tree_invisible="1"/>
 </tree>
diff -r 072daf920a7c -r 32265209fd9e trytond/res/ir.xml
--- a/trytond/res/ir.xml        Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/res/ir.xml        Fri Apr 12 15:59:25 2019 +0200
@@ -792,27 +792,10 @@
             <field name="rule_group" ref="rule_group_sequence_strict"/>
         </record>
 
-        <record model="res.user" id="user_trigger">
-            <field name="login">user_cron_trigger</field>
-            <field name="name">Cron Trigger</field>
-            <field name="active" eval="False"/>
-        </record>
-        <record model="res.user-res.group" id="user_trigger_group_admin">
-            <field name="user" ref="user_trigger"/>
-            <field name="group" ref="group_admin"/>
-        </record>
-
         <record model="ir.cron" id="cron_trigger_time">
-            <field name="name">Run On Time Triggers</field>
-            <field name="request_user" ref="user_admin"/>
-            <field name="user" ref="user_trigger"/>
-            <field name="active" eval="True"/>
+            <field name="method">ir.trigger|trigger_time</field>
             <field name="interval_number" eval="5"/>
             <field name="interval_type">minutes</field>
-            <field name="number_calls" eval="-1"/>
-            <field name="repeat_missed" eval="False"/>
-            <field name="model">ir.trigger</field>
-            <field name="function">trigger_time</field>
         </record>
 
         <record model="ir.model.access" id="rule_default_view_tree_state">

Reply via email to