add monthly recurrence options

Add a recurrence option for meetings that repeat monthly. Instantiate
the ones for the first weekdays of the month for now.

Change-Id: I0fa95653594dc5a28008630f57bee67b92537d29
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2018-10-08 10:00:12 -04:00
parent 816a1e4815
commit 6a21064401
6 changed files with 199 additions and 0 deletions

View File

@ -163,6 +163,14 @@ yaml2ical supports a number of possible frequency options:
* ``quadweekly-week-2``, ``quadweekly-alternate``: Occurs when ``ISOweek % 4 == 2``
* ``quadweekly-week-3``: Occurs when ``ISOweek % 4 == 3``
* Event occurs in the first week of a month:
* ``first-monday``: On the first Monday of the month.
* ``first-tuesday``: On the first Tuesday of the month.
* ``first-wednesday``: On the first Wednesday of the month.
* ``first-thursday``: On the first Thursday of the month.
* ``first-friday``: On the first Friday of the month.
* Event doesn't happen on a defined schedule but is used as a placeholder for
html generation:

View File

@ -147,6 +147,11 @@ class Schedule(object):
'quadweekly-week-2': set([2]),
'quadweekly-week-3': set([3]),
'quadweekly-alternate': set([2]),
'first-monday': set([0, 1, 2, 3]),
'first-tuesday': set([0, 1, 2, 3]),
'first-wednesday': set([0, 1, 2, 3]),
'first-thursday': set([0, 1, 2, 3]),
'first-friday': set([0, 1, 2, 3]),
}
return len(week[self.freq].intersection(week[other.freq])) > 0

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import calendar
import datetime
@ -131,6 +132,66 @@ class AdhocRecurrence(object):
def __str__(self):
return "Occurs as needed, no fixed schedule."
class MonthlyRecurrence(object):
"""Meetings occuring every month."""
def __init__(self, week, day):
self._week = week
self._day = day
def next_occurence(self, current_date_time, day):
"""Return the date of the next meeting.
:param current_date_time: datetime object of meeting
:param day: weekday the meeting is held on
:returns: datetime object of the next meeting time
"""
weekday = WEEKDAYS[day]
month = current_date_time.month + 1
year = current_date_time.year
if current_date_time.month == 12:
month = 1
year = year + 1
next_month_dates = calendar.monthcalendar(year, month)
# We can't simply index into the dates for the next month
# because we don't know that the first week is full of days
# that actually appear in that month. Therefore we loop
# through them counting down until we've skipped enough weeks.
skip_weeks = self._week - 1
for week in next_month_dates:
day = week[weekday]
# Dates in the week that fall in other months
# are 0 so we want to skip counting those weeks.
if not day:
continue
# If we have skipped all of the weeks we need to,
# we have the day.
if not skip_weeks:
return datetime.datetime(
year, month, day,
current_date_time.hour, current_date_time.minute,
current_date_time.second, current_date_time.microsecond,
)
skip_weeks -= 1
raise ValueError(
'Could not compute week {} of next month for {}'.format(
self._week, current_date_time)
)
def rrule(self):
return {
'freq': 'monthly',
'byday': '{}{}'.format(self._week, self._day[:2].upper()),
}
def __str__(self):
return "Monthly"
supported_recurrences = {
'weekly': WeeklyRecurrence(),
'biweekly-odd': BiWeeklyRecurrence(style='odd'),
@ -141,4 +202,9 @@ supported_recurrences = {
'quadweekly-week-3': QuadWeeklyRecurrence(week=3),
'quadweekly-alternate': QuadWeeklyRecurrence(week=2),
'adhoc': AdhocRecurrence(),
'first-monday': MonthlyRecurrence(week=1, day='Monday'),
'first-tuesday': MonthlyRecurrence(week=1, day='Tuesday'),
'first-wednesday': MonthlyRecurrence(week=1, day='Wednesday'),
'first-thursday': MonthlyRecurrence(week=1, day='Thursday'),
'first-friday': MonthlyRecurrence(week=1, day='Friday'),
}

View File

@ -362,3 +362,87 @@ chair: John Doe
description: >
Example Quadweekly Alternate meeting
"""
FIRST_MONDAY_MEETING = """
project: OpenStack Random Meeting
agenda_url: http://agenda.com/
project_url: http://project.com
schedule:
- time: '2200'
day: Monday
irc: openstack-meeting
frequency: first-monday
chair: John Doe
description: >
Example Monthly meeting
"""
FIRST_TUESDAY_MEETING = """
project: OpenStack Random Meeting
agenda_url: http://agenda.com/
project_url: http://project.com
schedule:
- time: '2200'
day: Tuesday
irc: openstack-meeting
frequency: first-tuesday
chair: John Doe
description: >
Example Monthly meeting
"""
WEEKLY_MEETING_2200 = """
project: OpenStack Subteam Meeting
schedule:
- time: '2200'
day: Wednesday
irc: openstack-meeting
frequency: weekly
chair: Joe Developer
description: >
Weekly meeting for Subteam project.
agenda: |
* Top bugs this week
"""
FIRST_WEDNESDAY_MEETING = """
project: OpenStack Random Meeting
agenda_url: http://agenda.com/
project_url: http://project.com
schedule:
- time: '2200'
day: Wednesday
irc: openstack-meeting
frequency: first-wednesday
chair: John Doe
description: >
Example Monthly meeting
"""
FIRST_THURSDAY_MEETING = """
project: OpenStack Random Meeting
agenda_url: http://agenda.com/
project_url: http://project.com
schedule:
- time: '2200'
day: Thursday
irc: openstack-meeting
frequency: first-thursday
chair: John Doe
description: >
Example Monthly meeting
"""
FIRST_FRIDAY_MEETING = """
project: OpenStack Random Meeting
agenda_url: http://agenda.com/
project_url: http://project.com
schedule:
- time: '2200'
day: Friday
irc: openstack-meeting
frequency: first-friday
chair: John Doe
description: >
Example Monthly meeting
"""

View File

@ -163,6 +163,20 @@ class MeetingTestCase(unittest.TestCase):
sample_data.CONFLICTING_WEEKLY_MEETING,
sample_data.MEETING_WITH_DURATION)
def test_monthly_conflicts(self):
self.should_be_conflicting(
sample_data.WEEKLY_MEETING_2200,
sample_data.FIRST_WEDNESDAY_MEETING)
self.should_be_conflicting(
sample_data.BIWEEKLY_EVEN_MEETING,
sample_data.FIRST_WEDNESDAY_MEETING)
self.should_be_conflicting(
sample_data.QUADWEEKLY_MEETING,
sample_data.FIRST_WEDNESDAY_MEETING)
self.should_be_conflicting(
sample_data.ALTERNATING_MEETING,
sample_data.FIRST_WEDNESDAY_MEETING)
def test_skip_meeting(self):
meeting_yaml = sample_data.MEETING_WITH_SKIP_DATES
# Copied from sample_data.MEETING_WITH_SKIP_DATES

View File

@ -83,3 +83,25 @@ class RecurrenceTestCase(unittest.TestCase):
self.assertEqual(
'Every four weeks on week %d of the four week rotation' % i,
str(recurrence.QuadWeeklyRecurrence(week=i)))
def test_monthly_first_week(self):
rec = recurrence.MonthlyRecurrence(week=1, day='Wednesday')
self.assertEqual(
datetime.datetime(2014, 11, 5, 2, 47, 28, 832666),
self.next_meeting(rec),
)
def test_monthly_second_week(self):
rec = recurrence.MonthlyRecurrence(week=2, day='Wednesday')
self.assertEqual(
datetime.datetime(2014, 11, 12, 2, 47, 28, 832666),
self.next_meeting(rec),
)
def test_monthly_invalid_week(self):
rec = recurrence.MonthlyRecurrence(week=6, day='Wednesday')
self.assertRaises(
ValueError,
self.next_meeting,
rec,
)