Commit 1b487f8c authored by Jérome Perrin's avatar Jérome Perrin

periodicity: fix infinite loop with impossible combinations of week and months

Some combinations of periodicity, for example repeat every first week
of the year and every month February are impossible (because the first
week of the year is always in January) and such configurations caused
infinite loops or probably overflow if we wait long enough.

The algorithm being to try the next day until all constraints are met,
it is not guaranteed to terminate.

To make sure the algorithm terminate, we rely on the fact that calendars
repeat after some time, so if after a few years we did not find a
matching combination, we can stop retrying.

according to https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week

> Each leap year repeats once every 28 years, and every common year
> repeats once every 6 years and twice every 11 years.

so trying for 28 years should be enough to see all combinations
parent f5be6109
Pipeline #19700 failed with stage
in 0 seconds
...@@ -181,13 +181,19 @@ class PresencePeriod(Movement, PeriodicityMixin): ...@@ -181,13 +181,19 @@ class PresencePeriod(Movement, PeriodicityMixin):
timezone = self._getTimezone(next_start_date) timezone = self._getTimezone(next_start_date)
next_start_date = self._getNextDay(next_start_date, timezone) next_start_date = self._getNextDay(next_start_date, timezone)
while 1:
# We use 366*28 below, because gregorian calendar repeat itself every 28 years, so we
# don't need to loop more than this if we don't find a date, because it might be an
# impossible combination of week and month (eg. week number 30 can not be in January)
for _ in range(366 * 28):
if (self._validateDay(next_start_date)) and \ if (self._validateDay(next_start_date)) and \
(self._validateWeek(next_start_date)) and \ (self._validateWeek(next_start_date)) and \
(self._validateMonth(next_start_date)): (self._validateMonth(next_start_date)):
break break
else: else:
next_start_date = self._getNextDay(next_start_date, timezone) next_start_date = self._getNextDay(next_start_date, timezone)
else:
return None
return DateTime( return DateTime(
next_start_date.year(), next_start_date.year(),
......
...@@ -1937,6 +1937,48 @@ class TestCalendar(ERP5ReportTestCase): ...@@ -1937,6 +1937,48 @@ class TestCalendar(ERP5ReportTestCase):
self.assertEqual([], assignment.asMovementList()) self.assertEqual([], assignment.asMovementList())
def test_GroupCalendarPeriodWeeksAndMonthPeriodicity(self):
"""Tests that combinations of periodicity weeks and months are handled correctly.
"""
node = self.portal.organisation_module.newContent(portal_type='Organisation',)
group_calendar = self.portal.group_calendar_module.newContent(
portal_type='Group Calendar')
group_calendar_period = group_calendar.newContent(
portal_type='Group Presence Period')
group_calendar_period.setStartDate('2000/01/01 08:00:00 UTC')
group_calendar_period.setStopDate('2000/01/01 09:00:00 UTC')
group_calendar_period.setQuantity(10)
group_calendar_period.setResourceValue(
self.portal.portal_categories.calendar_period_type.type1)
# this group calendar repeats every days of the first week of the year and the second
# months, which is impossible.
group_calendar_period.setPeriodicityWeekList((1, ))
group_calendar_period.setPeriodicityMonthList((2, ))
self.tic()
assignment = self.portal.group_calendar_assignment_module.newContent(
specialise_value=group_calendar,
resource_value=self.portal.portal_categories.calendar_period_type.type1,
start_date=DateTime('2000/01/01 08:00:00 UTC'),
stop_date=DateTime('2010/01/01 18:00:00 UTC'),
destination_value=node)
assignment.confirm()
self.tic()
self.assertFalse(assignment.asMovementList()) # ... and no infinite loop
# edge case, repeat every Friday of week 9 in February, this does not happen every year
group_calendar_period.setPeriodicityWeekList((9, ))
group_calendar_period.setPeriodicityMonthList((2, ))
group_calendar_period.setPeriodicityWeekDayList(['Friday'])
self.assertEqual(
[m.getStartDate() for m in assignment.asMovementList()],
[DateTime('2003/02/28 09:00:00 UTC'),
DateTime('2004/02/27 09:00:00 UTC'),
DateTime('2008/02/29 09:00:00 UTC'),
DateTime('2009/02/27 09:00:00 UTC'),])
def test_PersonModule_viewLeaveRequestReport(self): def test_PersonModule_viewLeaveRequestReport(self):
# in this test, type1 is the type for presences, type2 & type3 are types # in this test, type1 is the type for presences, type2 & type3 are types
# for leaves. # for leaves.
......
...@@ -276,6 +276,16 @@ class TestAlarm(ERP5TypeTestCase): ...@@ -276,6 +276,16 @@ class TestAlarm(ERP5TypeTestCase):
self.tic() self.tic()
self.checkDate(alarm, right_first_date, right_second_date, right_third_date,right_fourth_date) self.checkDate(alarm, right_first_date, right_second_date, right_third_date,right_fourth_date)
def test_week_and_month_impossible_combination(self):
alarm = self.newAlarm(enabled=True)
alarm.setPeriodicityStartDate(DateTime(2000, 1, 1))
# week 41 can not be in January
alarm.setPeriodicityWeekList((41, ))
alarm.setPeriodicityMonthList((1, ))
self.tic()
# next alarm date never advance
self.checkDate(alarm, DateTime(2000, 1, 1), DateTime(2000, 1, 1), DateTime(2000, 1, 1),)
def test_12_Every5Minutes(self): def test_12_Every5Minutes(self):
alarm = self.newAlarm(enabled=True) alarm = self.newAlarm(enabled=True)
now = DateTime() now = DateTime()
......
...@@ -203,7 +203,14 @@ class PeriodicityMixin: ...@@ -203,7 +203,14 @@ class PeriodicityMixin:
previous_date = next_start_date previous_date = next_start_date
next_start_date = max(self._getNextMinute(next_start_date, timezone), next_start_date = max(self._getNextMinute(next_start_date, timezone),
current_date) current_date)
while 1:
# We'll try every date to check if they validate the periodicity
# constraints, but there might not be any valid date (for example,
# repeat every 2nd week in August can never be validated). Because
# gregorian calendar repeat every 28 years, if we did not get a match
# in the next 28 years we stop looping.
max_date = next_start_date + (28 * 366)
while next_start_date < max_date:
if not self._validateMonth(next_start_date): if not self._validateMonth(next_start_date):
next_start_date = self._getNextMonth(next_start_date, timezone) next_start_date = self._getNextMonth(next_start_date, timezone)
elif not (self._validateDay(next_start_date) and elif not (self._validateDay(next_start_date) and
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment