diff --git a/README.rst b/README.rst index 9a1caa8..73038cd 100644 --- a/README.rst +++ b/README.rst @@ -258,14 +258,21 @@ meeting should be ommited from the ical schedule VERSION:2.0 PRODID:-//yaml2ical agendas//EN BEGIN:VEVENT + SUMMARY:CANCELLED: Example Project Meeting (20151026T210000Z) + DTSTART;VALUE=DATE-TIME:20151026T210000Z + DURATION:PT1H + DESCRIPTION:Face 2 Face meeting at some location + LOCATION:#openstack-meeting + END:VEVENT + 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 + EXDATE:20151026T210000Z + 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/Meeting + s/Example\nProject URL: https://wiki.openstack.org/wiki/Example LOCATION:#openstack-meeting RRULE:FREQ=WEEKLY END:VEVENT diff --git a/yaml2ical/ical.py b/yaml2ical/ical.py index 0fffff0..e2c9fab 100644 --- a/yaml2ical/ical.py +++ b/yaml2ical/ical.py @@ -34,14 +34,27 @@ class Yaml2IcalCalendar(icalendar.Calendar): """Add this meeting to the calendar.""" for sch in meeting.schedules: - # one Event per iCal file + self.add_schedule(meeting, sch) + + def add_schedule(self, meeting, sch, exdate=None): event = icalendar.Event() # NOTE(jotan): I think the summary field needs to be unique per # event in an ical file (at least, for it to work with # Google Calendar) - event.add('summary', meeting.project) + summary = meeting.project + # NOTE(tonyb): If we're adding an a place holder event for a + # cancelled meeting make that as obvious as possible in the + # summary. + if exdate: + # NOTE(tonyb): Because some iCal consumers require that the + # summary be unique, and adding multiple "CANCELLED: $x" + # entries would violate that rule, append the (UTC) + # timestamp for the cancelled meeting. + suffix = exdate.date_str + summary = 'CANCELLED: %s (%s)' % (summary, suffix) + event.add('summary', summary) event.add('location', '#' + sch.irc) # add ical description @@ -60,28 +73,38 @@ class Yaml2IcalCalendar(icalendar.Calendar): ical_descript = "\n".join((ical_descript, "Project URL: %s" % (meeting.extras['project_url']))) - event.add('description', ical_descript) - # get starting date - next_meeting = sch.recurrence.next_occurence(sch.start_date, - sch.day) - next_meeting_date = datetime.datetime(next_meeting.year, - next_meeting.month, - next_meeting.day, - sch.time.hour, - sch.time.minute, - tzinfo=pytz.utc) - event.add('dtstart', next_meeting_date) + # NOTE(tonyb): If we're adding an a place holder event for a + # cancelled meeting do not add an rrule and set dtstart to the + # skipped date. + if not exdate: + # get starting date + next_meeting = sch.recurrence.next_occurence(sch.start_date, + sch.day) + next_meeting_date = datetime.datetime(next_meeting.year, + next_meeting.month, + next_meeting.day, + sch.time.hour, + sch.time.minute, + tzinfo=pytz.utc) + event.add('dtstart', next_meeting_date) - # add recurrence rule - event.add('rrule', sch.recurrence.rrule()) + # add recurrence rule + event.add('rrule', sch.recurrence.rrule()) + event.add('description', ical_descript) + else: + event.add('dtstart', exdate.date) + event.add('description', exdate.reason) event.add('duration', datetime.timedelta(minutes=sch.duration)) # Add exdate (exclude date) if present - if hasattr(sch, 'skip_dates'): + if not exdate and hasattr(sch, 'skip_dates'): for skip_date in sch.skip_dates: event.add('exdate', skip_date.date) + # NOTE(tonyb): If this is a skipped meeting add a + # non-recurring event with an obvisous summary. + self.add_schedule(meeting, sch, exdate=skip_date) # add event to calendar self.add_component(event) diff --git a/yaml2ical/tests/test_meeting.py b/yaml2ical/tests/test_meeting.py index fc79fa8..8b9820b 100644 --- a/yaml2ical/tests/test_meeting.py +++ b/yaml2ical/tests/test_meeting.py @@ -94,12 +94,27 @@ class MeetingTestCase(unittest.TestCase): def test_skip_meeting(self): meeting_yaml = sample_data.MEETING_WITH_SKIP_DATES - p = re.compile('.*exdate:\s*20150810T120000', re.IGNORECASE) + # Copied from sample_data.MEETING_WITH_SKIP_DATES + summary = 'OpenStack Subteam 8 Meeting' + patterns = [] + # The "main" meeting should have an exdate + patterns.append(re.compile('.*exdate:\s*20150810T120000', re.I)) + # The "main" meeting should start on 2015-08-13 + patterns.append(re.compile('.*dtstart;.*:20150803T120000Z', re.I)) + # The "main" meeting should have a simple summary + patterns.append(re.compile('.*summary:\s*%s' % summary, re.I)) + # The "skipped" meeting should start on 20150806 + patterns.append(re.compile('.*dtstart;.*:20150810T120000Z', re.I)) + # The "skipped" meeting should say include 'CANCELLED' and the datetime + patterns.append(re.compile('.*summary:\s*CANCELLED.*20150810T120000Z', + re.I)) m = meeting.load_meetings(meeting_yaml)[0] cal = ical.Yaml2IcalCalendar() cal.add_meeting(m) + cal_str = str(cal.to_ical()) self.assertTrue(hasattr(m.schedules[0], 'skip_dates')) - self.assertNotEqual(None, p.match(str(cal.to_ical()))) + for p in patterns: + self.assertNotEqual(None, p.match(cal_str)) def test_skip_meeting_missing_skip_date(self): self.assertRaises(KeyError,