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:
parent
816a1e4815
commit
6a21064401
|
@ -163,6 +163,14 @@ yaml2ical supports a number of possible frequency options:
|
||||||
* ``quadweekly-week-2``, ``quadweekly-alternate``: Occurs when ``ISOweek % 4 == 2``
|
* ``quadweekly-week-2``, ``quadweekly-alternate``: Occurs when ``ISOweek % 4 == 2``
|
||||||
* ``quadweekly-week-3``: Occurs when ``ISOweek % 4 == 3``
|
* ``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
|
* Event doesn't happen on a defined schedule but is used as a placeholder for
|
||||||
html generation:
|
html generation:
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,11 @@ class Schedule(object):
|
||||||
'quadweekly-week-2': set([2]),
|
'quadweekly-week-2': set([2]),
|
||||||
'quadweekly-week-3': set([3]),
|
'quadweekly-week-3': set([3]),
|
||||||
'quadweekly-alternate': set([2]),
|
'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
|
return len(week[self.freq].intersection(week[other.freq])) > 0
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import calendar
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@ -131,6 +132,66 @@ class AdhocRecurrence(object):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Occurs as needed, no fixed schedule."
|
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 = {
|
supported_recurrences = {
|
||||||
'weekly': WeeklyRecurrence(),
|
'weekly': WeeklyRecurrence(),
|
||||||
'biweekly-odd': BiWeeklyRecurrence(style='odd'),
|
'biweekly-odd': BiWeeklyRecurrence(style='odd'),
|
||||||
|
@ -141,4 +202,9 @@ supported_recurrences = {
|
||||||
'quadweekly-week-3': QuadWeeklyRecurrence(week=3),
|
'quadweekly-week-3': QuadWeeklyRecurrence(week=3),
|
||||||
'quadweekly-alternate': QuadWeeklyRecurrence(week=2),
|
'quadweekly-alternate': QuadWeeklyRecurrence(week=2),
|
||||||
'adhoc': AdhocRecurrence(),
|
'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'),
|
||||||
}
|
}
|
||||||
|
|
|
@ -362,3 +362,87 @@ chair: John Doe
|
||||||
description: >
|
description: >
|
||||||
Example Quadweekly Alternate meeting
|
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
|
||||||
|
"""
|
||||||
|
|
|
@ -163,6 +163,20 @@ class MeetingTestCase(unittest.TestCase):
|
||||||
sample_data.CONFLICTING_WEEKLY_MEETING,
|
sample_data.CONFLICTING_WEEKLY_MEETING,
|
||||||
sample_data.MEETING_WITH_DURATION)
|
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):
|
def test_skip_meeting(self):
|
||||||
meeting_yaml = sample_data.MEETING_WITH_SKIP_DATES
|
meeting_yaml = sample_data.MEETING_WITH_SKIP_DATES
|
||||||
# Copied from sample_data.MEETING_WITH_SKIP_DATES
|
# Copied from sample_data.MEETING_WITH_SKIP_DATES
|
||||||
|
|
|
@ -83,3 +83,25 @@ class RecurrenceTestCase(unittest.TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'Every four weeks on week %d of the four week rotation' % i,
|
'Every four weeks on week %d of the four week rotation' % i,
|
||||||
str(recurrence.QuadWeeklyRecurrence(week=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,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue