#!/usr/bin/env python
#
# Simple iCalendar aggregator
#
# Copyright (C) 2010 Magnus Hagander
# Copyright (C) 2010 PostgreSQL Europe
#
# Released under the PostgreSQL Licence.
#
import sys
import urllib2
from ConfigParser import ConfigParser
from StringIO import StringIO
from datetime import datetime, timedelta
from pytz import timezone, utc
from string import capwords
class Event(object):
"""
Represents a single event in the schedule.
"""
def __init__(self, timezone, timezone_adjust):
"""
timezone and timezone_adjust are the same parameters as in the Aggregator.
"""
self.timezone = timezone
self.timezone_adjust = timezone_adjust
self.summary = None
self.start = None
self.end = None
self.location = None
def validate(self):
"""
Validate that we have all the required fields for this event.
Also validate taht we don't have any events going across multiple days,
since the schedule generator can't deal with that.
"""
if not self.summary: raise Exception('Summary not set')
if not self.start: raise Exception('Start not set')
if not self.end: raise Exception('End not set')
if self.start.astimezone(self.timezone).date() != self.end.astimezone(self.timezone).date():
raise Exception('Can\'t deal with cross-day events')
def setstart(self, v):
"""Set the start time for the event, by parsing an iCalendar style date"""
self.start = self._parse_time(v)
def getstart(self):
"""Return the start time for the event, in iCalendar format"""
return self._print_time(self.start)
def setend(self, v):
"""Set the end time for the event, by parsing an iCalendar style date"""
self.end = self._parse_time(v)
def getend(self):
"""Return the end time for the event, in iCalendar format"""
return self._print_time(self.end)
def _parse_time(self, v):
"""
Parse any iCalendar style datetime, and return it in a python object.
The time is always returned in UTC, but will be adjusted using the
timezone_adjust parameter if required.
"""
return utc.localize(datetime.strptime(v, "%Y%m%dT%H%M%SZ")) + timedelta(hours=self.timezone_adjust)
def _print_time(self, v):
"""
Convert a date to iCalendar format.
"""
return v.strftime("%Y%m%dT%H%M%SZ")
def __str__(self):
return "%s - %s: %s" % (self.start, self.end, self.summary)
def compare_events(a,b):
"""
Compare two Events, used as parameter when sorting a list of Events.
Comparison is done only on the start time.
"""
return cmp(a.start, b.start)
class IcalReader(object):
"""
Wrap simple reading of iCalendar data. The only thing it adds
on top of a regular reader is at this point dealing with continued
lines, where if a line starts with a space the contents are merged
with the previous line.
"""
def __init__(self, f):
self.lines = [l.rstrip() for l in f.readlines()]
def readline(self):
if not len(self.lines): return None
s = StringIO()
while True:
s.write(self.lines.pop(0).lstrip())
if not len(self.lines):
break
if not self.lines[0].startswith(" "):
break
return s.getvalue()
class Aggregator(object):
"""
Represents one set of iCalendar feeds being aggregated together to
form one schedule.
"""
def __init__(self, timezone, timezone_adjust):
"""
Initialize the Aggregator object.
timezone indicates which timezone to generate the output HTML
using (iCalendar feeds are always generated in UTC).
timezone_adjust allows for adjusting all *incoming* timestamps
in the parsed iCalendar feeds with a fixed number of hours, to
correct for incorrectly published feeds.
"""
self.feeds = []
self.events = []
self.timezone = timezone
self.timezone_adjust = timezone_adjust
def add_feed(self, name, url):
"""
Add a feed with room name "name" with an iCalendar feed at
url. Does not actually fetch the URL at this time.
"""
self.feeds.append((name, url))
def pull_all(self):
"""
Fetch all the iCalendar feeds specified, and parse them
to event objects store in the local list.
"""
for feed, url in self.feeds:
f = urllib2.urlopen(url)
for event in self._parse_ical(f):
event.location = feed
self.events.append(event)
self.events.sort(compare_events)
def generate_ical(self):
"""
Generate an iCalendar format file with all the events
in this aggregator.
Returns the iCalendar data as a string.
"""
f = StringIO()
f.write("""BEGIN:VCALENDAR\r
VERSION:2.0\r
PRODID:-//hagander/icalaggregator//NONSGML v1.0//EN\r
""")
for event in self.events:
f.write("BEGIN:VEVENT\r\n")
f.write("DTSTART:%s\r\n" % event.getstart())
f.write("DTEND:%s\r\n" % event.getend())
f.write("SUMMARY:%s\r\n" % event.summary)
f.write("LOCATION:%s\r\n" % (event.location or ''))
f.write("END:VEVENT\r\n")
f.write("END:VCALENDAR\r\n")
return f.getvalue()
def generate_html(self):
"""
Generate the core HTML schedule for the aggregated data. To look good,
it obviously needs to be wrapped in some header and footer data elsewhere.
Returns this HTML as a string.
Implementation uses lots of inefficient scans etc, but given that it's
never going to deal with more than hundreds of events at a time, it'll
be fast enough...
"""
col_width = 150 # Width of each column
headersize = 30 # Size of the header *for each day*
rooms = {}
days = []
f = StringIO()
for i in range(0, len(self.feeds)):
rooms[self.feeds[i][0]] = i
for e in self.events:
if not e.start.date() in days:
days.append(e.start.date())
for day in sorted(days):
# First out first and last this day (yes, it's inefficient, but we don't
# have much data to deal with..)
firsttime = None
lasttime = None
for e in self.events:
if not e.start.date() == day: continue
if not firsttime: firsttime = e.start
lasttime = e.end
f.write("