Add support for skipping/excluding meetings

In the OpenStack community it's common to cancel meetings while the
summit happens.

Add the ability to add a list of dates to skip.  These will then be
included in the iCal

Change-Id: I1dd5ca6f3e5d6d257489fdc14bbe108abc7436e6
This commit is contained in:
Tony Breeds 2015-10-08 14:23:05 +11:00
parent faabbec1a6
commit ddf64aef6e
5 changed files with 203 additions and 2 deletions

View File

@ -123,6 +123,11 @@ Each meeting consists of:
* ``day``: the day of week the meeting takes place [MANDATORY] * ``day``: the day of week the meeting takes place [MANDATORY]
* ``irc``: the irc room in which the meeting is held [MANDATORY] * ``irc``: the irc room in which the meeting is held [MANDATORY]
* ``frequency``: frequent occurrence of the meeting [MANDATORY] * ``frequency``: frequent occurrence of the meeting [MANDATORY]
* ``skip_dates``: A set of dates that the meeting **DOES NOT** happen on
* ``skip_date``: Skip the meeting for specified date.
Format as ``start_date``
* ``reason``: A comment for why the meeting was skipped
* ``chair``: name of the meeting's chair [MANDATORY] * ``chair``: name of the meeting's chair [MANDATORY]
* ``description``: a paragraph description about the meeting [MANDATORY] * ``description``: a paragraph description about the meeting [MANDATORY]
* ``agenda_url``: a link to the agenda page for the meeting * ``agenda_url``: a link to the agenda page for the meeting
@ -137,8 +142,9 @@ templates, making it easy to build links to agenda pages for the
meeting or logs of past meetings. In the template file, use meeting or logs of past meetings. In the template file, use
``meeting.extras.name`` to access the value. ``meeting.extras.name`` to access the value.
Example
------- Example 1
---------
This is an example for the yaml meeting for Nova team meeting. The whole file This is an example for the yaml meeting for Nova team meeting. The whole file
will be import into Python as a dictionary. will be import into Python as a dictionary.
@ -204,3 +210,63 @@ will be import into Python as a dictionary.
:: ::
project_url: https://wiki.openstack.org/wiki/Nova project_url: https://wiki.openstack.org/wiki/Nova
* An extra property containing the MeetBot #startmeeting ID for the project is
saved in ``meeting_id`` and can be accessed in the template file as
``meeting.extras.meeting_id``.
::
meeting_id: nova
Example 2
---------
The following shows a complete YAML file for the IRC meetings for "example
project". The project starts holding weekly meetings from October 1st, the
project team has a "face to face" meeting on the 26th of October so that IRC
meeting should be ommited from the ical schedule
* This YAML
::
project: Example Project Meeting
project_url: https://wiki.openstack.org/wiki/Example
agenda_url: https://wiki.openstack.org/wiki/Meetings/Example
meeting_id: example
chair: A. Random Developer
description: >
This meeting is a weekly gathering of developers working on Example
project.
schedule:
- time: '2100'
day: Monday
irc: openstack-meeting
start_date: 20151001
frequency: weekly
skip_dates:
- skip_date: 20151026
reason: Face 2 Face meeting at some location
* Is converted into this iCal
::
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//yaml2ical agendas//EN
BEGIN:VEVENT
SUMMARY:Example Project Meeting
DTSTART;VALUE=DATE-TIME:20151005T210000Z
DURATION:PT1H
EXDATE:20151026T210000
DESCRIPTION:Project: Example Project Meeting\\nChair: A. Random Developer
\\nDescription: This meeting is a weekly gathering of developers working o
n Example project.\\n\\nAgenda URL: https://wiki.openstack.org/wiki/Meetin
gs/Example\\nProject URL: https://wiki.openstack.org/wiki/Example
LOCATION:#openstack-meeting
RRULE:FREQ=WEEKLY
END:VEVENT
END:VCALENDAR

View File

@ -78,6 +78,11 @@ class Yaml2IcalCalendar(icalendar.Calendar):
event.add('duration', datetime.timedelta(minutes=sch.duration)) event.add('duration', datetime.timedelta(minutes=sch.duration))
# Add exdate (exclude date) if present
if hasattr(sch, 'skip_dates'):
for skip_date in sch.skip_dates:
event.add('exdate', skip_date.date)
# add event to calendar # add event to calendar
self.add_component(event) self.add_component(event)

View File

@ -14,6 +14,7 @@ import datetime
from io import StringIO from io import StringIO
import os import os
import os.path import os.path
import pytz
import yaml import yaml
@ -31,6 +32,19 @@ DATES = {
ONE_WEEK = datetime.timedelta(weeks=1) ONE_WEEK = datetime.timedelta(weeks=1)
class SkipDate(object):
"""A date, time and reason to skip a meeting."""
def __init__(self, date, time, reason):
date = datetime.datetime.combine(date, time).replace(tzinfo=pytz.utc)
self.date = date
self.reason = reason
@property
def date_str(self):
return self.date.strftime("%Y%m%dT%H%M%SZ")
class Schedule(object): class Schedule(object):
"""A meeting schedule.""" """A meeting schedule."""
@ -77,6 +91,30 @@ class Schedule(object):
if self.day not in DATES.keys(): if self.day not in DATES.keys():
raise ValueError("'%s' is not a valid day of the week") raise ValueError("'%s' is not a valid day of the week")
# optional: skip_dates
# This is a sequence of mappings (YAML)
# This is a list of dicts (python)
if 'skip_dates' in sched_yaml:
self.skip_dates = []
for skip_date in sched_yaml['skip_dates']:
missing_keys = set(['skip_date', 'reason']) - set(skip_date)
if missing_keys:
raise KeyError(("Processing: %s Missing keys - %s" %
(self.filefrom, ','.join(missing_keys))))
# NOTE(tonyb) We need to include the time in an exdate
# without it the excluded occurrence never matches a
# scheduled occurrence.
try:
date_str = str(skip_date['skip_date'])
date = datetime.datetime.strptime(date_str, '%Y%m%d')
self.skip_dates.append(SkipDate(date, self.time.time(),
skip_date['reason']))
except ValueError:
raise ValueError(("Processing: %s Could not parse "
"skip_date - %s" %
(self.filefrom, skip_date['skip_date'])))
# NOTE(tonyb): We need to do this datetime shenanigans is so we can # NOTE(tonyb): We need to do this datetime shenanigans is so we can
# deal with meetings that start on day1 and end on day2. # deal with meetings that start on day1 and end on day2.
self.meeting_start = datetime.datetime.combine(DATES[self.day], self.meeting_start = datetime.datetime.combine(DATES[self.day],

View File

@ -192,3 +192,69 @@ description: >
agenda: | agenda: |
* Debate whether this should be a longer meeting * Debate whether this should be a longer meeting
""" """
MEETING_WITH_SKIP_DATES = """
project: OpenStack Subteam 8 Meeting
schedule:
- time: '1200'
day: Monday
start_date: 20150801
irc: openstack-meeting
frequency: weekly
skip_dates:
- skip_date: 20150810
reason: Chair on vacation
chair: Shannon Stacker
description: >
Weekly short meeting for Subteam project.
"""
MEETING_WITH_SKIP_DATES_BAD_DATE = """
project: OpenStack Subteam 8 Meeting
schedule:
- time: '1200'
day: Monday
start_date: 20150801
irc: openstack-meeting
frequency: weekly
skip_dates:
- skip_date: 2015080
reason: Chair on vacation
chair: Shannon Stacker
description: >
Weekly short meeting for Subteam project.
"""
# typo in skip_date
MEETING_WITH_MISSING_SKIP_DATE = """
project: OpenStack Subteam 8 Meeting
schedule:
- time: '1200'
day: Monday
start_date: 20150801
irc: openstack-meeting
frequency: weekly
skip_dates:
- skiip_date: 20150806
reason: Chair on vacation
chair: Shannon Stacker
description: >
Weekly short meeting for Subteam project.
"""
# typo in reason
MEETING_WITH_MISSING_REASON = """
project: OpenStack Subteam 8 Meeting
schedule:
- time: '1200'
day: Monday
start_date: 20150801
irc: openstack-meeting
frequency: weekly
skip_dates:
- skip_date: 20150806
reaso: Chair on vacation
chair: Shannon Stacker
description: >
Weekly short meeting for Subteam project.
"""

View File

@ -10,8 +10,10 @@
# 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 re
import unittest import unittest
from yaml2ical import ical
from yaml2ical import meeting from yaml2ical import meeting
from yaml2ical.tests import sample_data from yaml2ical.tests import sample_data
@ -89,3 +91,27 @@ class MeetingTestCase(unittest.TestCase):
self.should_not_conflict( self.should_not_conflict(
sample_data.CONFLICTING_WEEKLY_MEETING, sample_data.CONFLICTING_WEEKLY_MEETING,
sample_data.MEETING_WITH_DURATION) sample_data.MEETING_WITH_DURATION)
def test_skip_meeting(self):
meeting_yaml = sample_data.MEETING_WITH_SKIP_DATES
p = re.compile('.*exdate:\s*20150810T120000', re.IGNORECASE)
m = meeting.load_meetings(meeting_yaml)[0]
cal = ical.Yaml2IcalCalendar()
cal.add_meeting(m)
self.assertTrue(hasattr(m.schedules[0], 'skip_dates'))
self.assertNotEqual(None, p.match(str(cal.to_ical())))
def test_skip_meeting_missing_skip_date(self):
self.assertRaises(KeyError,
meeting.load_meetings,
sample_data.MEETING_WITH_MISSING_SKIP_DATE)
def test_skip_meeting_missing_reason(self):
self.assertRaises(KeyError,
meeting.load_meetings,
sample_data.MEETING_WITH_MISSING_REASON)
def test_skip_meeting_bad_skip_date(self):
self.assertRaises(ValueError,
meeting.load_meetings,
sample_data.MEETING_WITH_SKIP_DATES_BAD_DATE)