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:
parent
faabbec1a6
commit
ddf64aef6e
70
README.rst
70
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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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],
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user