Schedule actions using server timezone
issue9436 review423091003
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -13,3 +13,4 @@ Tools API reference.
|
||||
email
|
||||
singleton
|
||||
immutabledict
|
||||
timezone
|
||||
|
||||
@@ -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``.
|
||||
@@ -11,6 +11,11 @@ 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.
|
||||
|
||||
.. 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``
|
||||
|
||||
@@ -161,6 +161,7 @@ setup(name=name,
|
||||
'werkzeug',
|
||||
'wrapt',
|
||||
'passlib >= 1.7.0',
|
||||
'pytz;python_verrion<"3.9"',
|
||||
],
|
||||
extras_require={
|
||||
'test': tests_require,
|
||||
|
||||
@@ -9,6 +9,7 @@ from lxml import etree, objectify
|
||||
|
||||
__version__ = "6.5.0"
|
||||
|
||||
os.environ.setdefault('TRYTOND_TZ', os.environ.get('TZ', 'UTC'))
|
||||
os.environ['TZ'] = 'UTC'
|
||||
if hasattr(time, 'tzset'):
|
||||
time.tzset()
|
||||
|
||||
+7
-2
@@ -14,6 +14,7 @@ from trytond.model import (
|
||||
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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
['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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
# 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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
]
|
||||
|
||||
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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
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
|
||||
|
||||
@@ -21,6 +21,8 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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"/>
|
||||
|
||||
@@ -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 dateutil.relativedelta import relativedelta
|
||||
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 IrTestCase(ModuleTestCase):
|
||||
})
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -14,7 +14,7 @@ from trytond.tools import (
|
||||
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 @@ class ToolsTestCase(unittest.TestCase):
|
||||
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"
|
||||
|
||||
@@ -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'])
|
||||
Reference in New Issue
Block a user