diff --git a/README.rst b/README.rst index 0958dba..9a1caa8 100644 --- a/README.rst +++ b/README.rst @@ -123,6 +123,11 @@ Each meeting consists of: * ``day``: the day of week the meeting takes place [MANDATORY] * ``irc``: the irc room in which the meeting is held [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] * ``description``: a paragraph description about the meeting [MANDATORY] * ``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.extras.name`` to access the value. -Example -------- + +Example 1 +--------- This is an example for the yaml meeting for Nova team meeting. The whole file 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 + +* 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 diff --git a/yaml2ical/ical.py b/yaml2ical/ical.py index 5632eb5..0fffff0 100644 --- a/yaml2ical/ical.py +++ b/yaml2ical/ical.py @@ -78,6 +78,11 @@ class Yaml2IcalCalendar(icalendar.Calendar): 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 self.add_component(event) diff --git a/yaml2ical/meeting.py b/yaml2ical/meeting.py index badeb55..8d4532e 100644 --- a/yaml2ical/meeting.py +++ b/yaml2ical/meeting.py @@ -14,6 +14,7 @@ import datetime from io import StringIO import os import os.path +import pytz import yaml @@ -31,6 +32,19 @@ DATES = { 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): """A meeting schedule.""" @@ -77,6 +91,30 @@ class Schedule(object): if self.day not in DATES.keys(): 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 # deal with meetings that start on day1 and end on day2. self.meeting_start = datetime.datetime.combine(DATES[self.day], diff --git a/yaml2ical/tests/sample_data.py b/yaml2ical/tests/sample_data.py index cfd9c86..4985dea 100644 --- a/yaml2ical/tests/sample_data.py +++ b/yaml2ical/tests/sample_data.py @@ -192,3 +192,69 @@ description: > agenda: | * 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. +""" diff --git a/yaml2ical/tests/test_meeting.py b/yaml2ical/tests/test_meeting.py index 489fe40..fc79fa8 100644 --- a/yaml2ical/tests/test_meeting.py +++ b/yaml2ical/tests/test_meeting.py @@ -10,8 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import re import unittest +from yaml2ical import ical from yaml2ical import meeting from yaml2ical.tests import sample_data @@ -89,3 +91,27 @@ class MeetingTestCase(unittest.TestCase): self.should_not_conflict( sample_data.CONFLICTING_WEEKLY_MEETING, 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)