changeset b040256012ac in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset&node=b040256012ac
description:
        Schedule actions using server timezone

        issue9436
        review423091003
diffstat:

 CHANGELOG                     |   1 +
 doc/ref/tools/index.rst       |   1 +
 doc/ref/tools/timezone.rst    |  25 ++++++++++++++
 doc/topics/cron.rst           |   5 ++
 setup.py                      |   1 +
 trytond/__init__.py           |   1 +
 trytond/ir/cron.py            |   9 +++-
 trytond/ir/view/cron_form.xml |   2 +
 trytond/tests/test_ir.py      |  76 ++++++++++++++++++++++++++++++++++++++++++-
 trytond/tests/test_tools.py   |  14 +++++++-
 trytond/tools/timezone.py     |  45 +++++++++++++++++++++++++
 11 files changed, 176 insertions(+), 4 deletions(-)

diffs (321 lines):

diff -r f4c77eb02b5b -r b040256012ac CHANGELOG
--- a/CHANGELOG Mon Oct 03 00:15:12 2022 +0200
+++ b/CHANGELOG Mon Oct 03 00:20:49 2022 +0200
@@ -1,3 +1,4 @@
+* Schedule actions using server timezone
 * Reset the user sessions less often
 * Add straight values to wizard state view
 * Add strip option to Char fields
diff -r f4c77eb02b5b -r b040256012ac doc/ref/tools/index.rst
--- a/doc/ref/tools/index.rst   Mon Oct 03 00:15:12 2022 +0200
+++ b/doc/ref/tools/index.rst   Mon Oct 03 00:20:49 2022 +0200
@@ -13,3 +13,4 @@
     email
     singleton
     immutabledict
+    timezone
diff -r f4c77eb02b5b -r b040256012ac doc/ref/tools/timezone.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/ref/tools/timezone.rst        Mon Oct 03 00:20:49 2022 +0200
@@ -0,0 +1,25 @@
+.. _ref-tools-timezone:
+.. module:: trytond.tools.timezone
+
+timezone
+========
+
+.. function:: get_tzinfo(zoneid)
+
+   Get a class representing a IANA time zone specified by the string 
``zoneid``.
+
+.. function:: available_timezones()
+
+   Get a list of all the valid IANA keys available.
+
+.. attribute:: UTC
+
+   The UTC :py:class:`datetime.tzinfo` instance.
+
+.. attribute:: SERVER
+
+   The server timezone :py:class:`datetime.tzinfo` instance.
+
+   Tryton tests the environment variables ``TRYTOND_TZ`` and ``TZ`` in this
+   order to select to IANA key to use.
+   If they are both empty, it defaults to ``UTC``.
diff -r f4c77eb02b5b -r b040256012ac doc/topics/cron.rst
--- a/doc/topics/cron.rst       Mon Oct 03 00:15:12 2022 +0200
+++ b/doc/topics/cron.rst       Mon Oct 03 00:20:49 2022 +0200
@@ -11,6 +11,11 @@
 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.
 
+.. note::
+
+   The timezone used to schedule the action is
+   :attr:`timezone.SERVER <trytond.tools.timezone.SERVER>`.
+
 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``
diff -r f4c77eb02b5b -r b040256012ac setup.py
--- a/setup.py  Mon Oct 03 00:15:12 2022 +0200
+++ b/setup.py  Mon Oct 03 00:20:49 2022 +0200
@@ -161,6 +161,7 @@
         'werkzeug',
         'wrapt',
         'passlib >= 1.7.0',
+        'pytz;python_verrion<"3.9"',
         ],
     extras_require={
         'test': tests_require,
diff -r f4c77eb02b5b -r b040256012ac trytond/__init__.py
--- a/trytond/__init__.py       Mon Oct 03 00:15:12 2022 +0200
+++ b/trytond/__init__.py       Mon Oct 03 00:20:49 2022 +0200
@@ -9,6 +9,7 @@
 
 __version__ = "6.5.0"
 
+os.environ.setdefault('TRYTOND_TZ', os.environ.get('TZ', 'UTC'))
 os.environ['TZ'] = 'UTC'
 if hasattr(time, 'tzset'):
     time.tzset()
diff -r f4c77eb02b5b -r b040256012ac trytond/ir/cron.py
--- a/trytond/ir/cron.py        Mon Oct 03 00:15:12 2022 +0200
+++ b/trytond/ir/cron.py        Mon Oct 03 00:20:49 2022 +0200
@@ -14,6 +14,7 @@
 from trytond.pool import Pool
 from trytond.pyson import Eval
 from trytond.status import processing
+from trytond.tools import timezone as tz
 from trytond.transaction import Transaction
 from trytond.worker import run_task
 
@@ -66,6 +67,7 @@
                 ['minutes', 'hours', 'days', 'weeks']),
             },
         depends=['interval_type'])
+    timezone = fields.Function(fields.Char("Timezone"), 'get_timezone')
 
     next_call = fields.DateTime("Next Call", select=True)
     method = fields.Selection([
@@ -97,6 +99,9 @@
         # Migration from 5.0: remove required on next_call
         table_h.not_null_action('next_call', 'remove')
 
+    def get_timezone(self, name):
+        return tz.SERVER.key
+
     @staticmethod
     def check_xml_record(crons, values):
         return True
@@ -110,7 +115,7 @@
             ]
 
     def compute_next_call(self, now):
-        return (now
+        return (now.replace(tzinfo=tz.UTC).astimezone(tz.SERVER)
             + relativedelta(**{self.interval_type: self.interval_number})
             + relativedelta(
                 microsecond=0,
@@ -132,7 +137,7 @@
                     int(self.weekday.index)
                     if self.weekday
                     and self.interval_type not in {'minutes', 'hours', 'days'}
-                    else None)))
+                    else None))).astimezone(tz.UTC).replace(tzinfo=None)
 
     @dualmethod
     @ModelView.button
diff -r f4c77eb02b5b -r b040256012ac trytond/ir/view/cron_form.xml
--- a/trytond/ir/view/cron_form.xml     Mon Oct 03 00:15:12 2022 +0200
+++ b/trytond/ir/view/cron_form.xml     Mon Oct 03 00:20:49 2022 +0200
@@ -21,6 +21,8 @@
         <field name="hour" xexpand="0"/>
         <label name="minute"/>
         <field name="minute" xexpand="0"/>
+        <label name="timezone"/>
+        <field name="timezone"/>
     </group>
     <label name="next_call"/>
     <field name="next_call"/>
diff -r f4c77eb02b5b -r b040256012ac trytond/tests/test_ir.py
--- a/trytond/tests/test_ir.py  Mon Oct 03 00:15:12 2022 +0200
+++ b/trytond/tests/test_ir.py  Mon Oct 03 00:20:49 2022 +0200
@@ -1,6 +1,7 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
 import datetime
+import unittest
 from decimal import Decimal
 from unittest.mock import ANY, Mock, patch
 
@@ -9,9 +10,11 @@
 from trytond.config import config
 from trytond.pool import Pool
 from trytond.pyson import Eval, If, PYSONEncoder
+from trytond.tools import timezone
 from trytond.transaction import Transaction
 
-from .test_tryton import ModuleTestCase, with_transaction
+from .test_tryton import (
+    ModuleTestCase, activate_module, drop_db, with_transaction)
 
 
 class IrTestCase(ModuleTestCase):
@@ -383,4 +386,75 @@
                 })
 
 
+class IrCronTestCase(unittest.TestCase):
+    "Test ir.cron features"
+
+    @classmethod
+    def setUpClass(cls):
+        drop_db()
+        activate_module(['ir'])
+        super().setUpClass()
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        drop_db()
+
+    def setUp(self):
+        server_tz = timezone.SERVER
+        timezone.SERVER = timezone.ZoneInfo('Canada/Eastern')
+        self.addCleanup(setattr, timezone, 'SERVER', server_tz)
+
+    def _get_cron(self):
+        pool = Pool()
+        Cron = pool.get('ir.cron')
+
+        cron = Cron()
+        for attribute in [
+                'interval_number', 'interval_type', 'minute', 'hour',
+                'weekday', 'day']:
+            setattr(cron, attribute, None)
+        return cron
+
+    @with_transaction()
+    def test_scheduling_non_utc(self):
+        "Test scheduling with a non UTC timezone"
+        cron = self._get_cron()
+        cron.interval_number = 1
+        cron.interval_type = 'days'
+        cron.hour = 1
+
+        # Quebec is UTC-5
+        self.assertEqual(
+            cron.compute_next_call(datetime.datetime(2021, 12, 31, 5, 0)),
+            datetime.datetime(2022, 1, 1, 6, 0))
+
+    @with_transaction()
+    def test_scheduling_on_dst_change(self):
+        "Test scheduling while the DST change occurs"
+        cron = self._get_cron()
+        cron.interval_number = 1
+        cron.interval_type = 'days'
+        cron.hour = 2
+
+        # 2022-03-13 is the day of DST switch
+        # Quebec is UTC-4
+        self.assertEqual(
+            cron.compute_next_call(datetime.datetime(2022, 3, 12, 6, 30)),
+            datetime.datetime(2022, 3, 13, 7, 30))
+
+    @with_transaction()
+    def test_scheduling_on_standard_time(self):
+        "Test scheduling while the calendar returns to the standard time"
+        cron = self._get_cron()
+        cron.interval_number = 1
+        cron.interval_type = 'hours'
+
+        # 2022-11-06 is the day of DST switch
+        # Quebec is UTC-5
+        self.assertEqual(
+            cron.compute_next_call(datetime.datetime(2022, 11, 6, 7, 30)),
+            datetime.datetime(2022, 11, 6, 8, 30))
+
+
 del ModuleTestCase
diff -r f4c77eb02b5b -r b040256012ac trytond/tests/test_tools.py
--- a/trytond/tests/test_tools.py       Mon Oct 03 00:15:12 2022 +0200
+++ b/trytond/tests/test_tools.py       Mon Oct 03 00:20:49 2022 +0200
@@ -14,7 +14,7 @@
     decimal_, escape_wildcard, file_open, firstline, grouped_slice,
     is_full_text, is_instance_method, lstrip_wildcard, reduce_domain,
     reduce_ids, remove_forbidden_chars, rstrip_wildcard, slugify,
-    sortable_values, strip_wildcard, unescape_wildcard)
+    sortable_values, strip_wildcard, timezone, unescape_wildcard)
 from trytond.tools.domain_inversion import (
     concat, domain_inversion, eval_domain, extract_reference_models,
     localize_domain, merge, parse, prepare_reference_domain, simplify,
@@ -291,6 +291,18 @@
             with self.subTest(string=string):
                 self.assertEqual(remove_forbidden_chars(string), result)
 
+    def test_get_tzinfo_valid(self):
+        "Test get_tzinfo with an valid timezone"
+        zi = timezone.get_tzinfo('Europe/Brussels')
+        now = dt.datetime(2022, 5, 17, tzinfo=zi)
+        self.assertEqual(str(now), "2022-05-17 00:00:00+02:00")
+
+    def test_get_tzinfo_invalid(self):
+        "Test get_tzinfo with an invalid timezone"
+        zi = timezone.get_tzinfo('foo')
+        now = dt.datetime(2022, 5, 17, tzinfo=zi)
+        self.assertEqual(str(now), "2022-05-17 00:00:00+00:00")
+
 
 class StringPartitionedTestCase(unittest.TestCase):
     "Test StringPartitioned"
diff -r f4c77eb02b5b -r b040256012ac trytond/tools/timezone.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/tools/timezone.py Mon Oct 03 00:20:49 2022 +0200
@@ -0,0 +1,45 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+import logging
+import os
+
+try:
+    import zoneinfo
+    ZoneInfo = zoneinfo.ZoneInfo
+    ZoneInfoNotFoundError = zoneinfo.ZoneInfoNotFoundError
+except ImportError:
+    import pytz
+    from dateutil.tz import gettz as ZoneInfo
+
+    class ZoneInfoNotFoundError(KeyError):
+        pass
+
+__all__ = ['SERVER', 'UTC', 'get_tzinfo', 'available_timezones']
+logger = logging.getLogger(__name__)
+_ALL_ZONES = None
+
+
+def available_timezones():
+    global _ALL_ZONES
+
+    if not _ALL_ZONES:
+        if zoneinfo:
+            _ALL_ZONES = zoneinfo.available_timezones()
+        else:
+            _ALL_ZONES = pytz.all_timezones
+    return _ALL_ZONES[:]
+
+
+def get_tzinfo(zoneid):
+    try:
+        zi = ZoneInfo(zoneid)
+        if not zi:
+            raise ZoneInfoNotFoundError
+    except ZoneInfoNotFoundError:
+        logger.warning("Timezone %s not found falling back to UTC", zoneid)
+        zi = UTC
+    return zi
+
+
+UTC = ZoneInfo('UTC')
+SERVER = get_tzinfo(os.environ['TRYTOND_TZ'])

Reply via email to