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'])