UNB/ CS/ David Bremner/ software/ hacks/ freebusy.py
#!/usr/bin/python

# $Id: freebusy.py 6090 2007-02-20 15:44:06Z bremner $
# This festering piece of hackery was written by David Bremner, bremner@unb.ca
# it is placed in the public domain
#
# It may destroy all of your files, ruin your credit rating, put you on the
# no-fly lists of 103 nations and transform your favourite relative into
# a camel.
#
# Don't say I didn't warn you.
#
# If you're not scared off yet, you need the iCalendar python package
# from http://codespeak.net/icalendar, as well as pytz and elementtree
# packages this are semi-standard, and are e.g. in the debian repositories.
#
# you will also need to create a directory ~/.freebusy and put
# schedule.vfb there (e.g. export from Korganizer)
#
# Release 0.3

from icalendar import Calendar, Event
from datetime import timedelta,time,datetime
from pytz import timezone,utc
import elementtree.ElementTree as ET
from ConfigParser import ConfigParser

from  os.path import expanduser
import os.path

daynames=['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']

class calpage(object):

    _config=dict(
	busystyle='{ background-color: red ; border-style: solid; border-color: gray ; border-width: thin  }',
	idlestyle='{ background-color: gray }',
	calfile=expanduser('~/.freebusy/schedule.vfb'),
	outfile=expanduser('~/.freebusy/schedule.html'),
	lastrun=expanduser('~/.freebusy/lastrun'),
	tzname='UTC',
	starthour=9,
	dayhours=9,
        maxlines=10,
	deltaminutes=15,
	pagetitle='Schedule',
        alwaysrun=True
	)

    def __getattr__(self,attr):
	return self._config[attr]
    
    def __init__(self,**config):
	self._day={}

	for k,v in config.items():
	    if not self._config.has_key(k):
		raise UserWarning('Unknown parameter '+k)
	    
	    self._config[k]=v

	self.localtz=timezone(self.tzname)
	self.delta= timedelta(minutes=self.deltaminutes)	
	self._html_init()



    def _html_init(self):
    
	self._root = ET.Element("html")
	head = ET.SubElement(self._root, "head")
	style=ET.SubElement(head,"style",
			    {'type':'text/css'})
	style.text='TD { text-align: center; border-style:none }'
	style.text+= 'TD.busy '+self.busystyle
	style.text+= 'TD.idle '+self.idlestyle
	

	title = ET.SubElement(head, "title")
	title.text = self.pagetitle

	body = ET.SubElement(self._root, "body")
	btitle=ET.SubElement(body,"h1")
	btitle.text=self.pagetitle
	ET.SubElement(body,"h2").text='Last modified '+datetime.now(self.localtz).isoformat()
	
	self._table= ET.SubElement(body,"table")

    def add_row(self,attr={}):
	self._cur_row=ET.SubElement(self._table,"tr",attr)

    def add_element(self,text,attr={}):
	elt=ET.SubElement(self._cur_row,"td",attr)
	elt.text=str(text)

    
    def cal_header(self):
	self.add_row()
	self.add_element('')
        self.add_element('')
	
	for hour in range(self.starthour,self.starthour+self.dayhours):
	    self.add_element("%02d" % hour)
	    for skip in range(1,60/self.deltaminutes):
		self.add_element('')

	self.add_row()
	self.add_element('')
       	self.add_element('')
	for hour in range(self.starthour,self.starthour+self.dayhours):
	    self.add_element('')
	    for min in range(self.deltaminutes,60,self.deltaminutes):
		self.add_element(min)
	
    def cal_row(self,today):
	hash=self._day[today]
	starttime=time(hour=self.starthour)
	startofday=datetime.combine(today,starttime)
	startofday =self.localtz.localize(startofday).astimezone(utc)

	endofday=startofday+timedelta(hours=self.dayhours)

        minutes=0
        daytime=startofday
	while (daytime < endofday):
	    if hash.has_key(daytime.time()):
		minutes+=self.deltaminutes
                
	    daytime+=self.delta


        if minutes==0:
            return
        
        daytime=startofday
	self.add_row()
	self.add_element(today.isoformat());
        self.add_element(daynames[today.weekday()])
        
		
	while (daytime < endofday):
	    if hash.has_key(daytime.time()):
		self.add_element('*',{'class':'busy'})
	    else:
		self.add_element('_',{'class':'idle'})
		
	    daytime+=self.delta

    def write(self,filename=None):

	if (filename !=None):
	    self.outfile=filename
	    
	days=self._day.keys()
	days.sort()
        lines=self.maxlines+1
	for today in days:
            if lines>self.maxlines:
                lines=1
                self.cal_header()
            else:
                lines += 1
                
	    self.cal_row(today)

	# wrap it in an ElementTree instance, and save as XML
	tree = ET.ElementTree(self._root)
	tree.write(self.outfile)


    def process_interval(self,start,end):
	date=start.date()
	if not self._day.has_key(date):
	    self._day[date]={}
		
	time=start
        # note, assumption delta is less than 1 hour
        offset=time.minute % (self.delta.seconds/60)
        if ( offset != 0):
            time=time.replace(minute=time.minute - offset)

	    
	while (time < end):
	    if time.date() != date:
		date=time.date()
	    if not self._day.has_key(date):
		self._day[date]={}
			
	    self._day[date][time.time()]=1
	    time += self.delta

            
    def parse(self,name=None):
	if (name != None):
	    self.calfile=name
	    
	cal = Calendar.from_string(open(self.calfile,'rb').read())	    
	for component in cal.walk('VFREEBUSY'):
	    for start,end in component.decoded('FREEBUSY'):
		self.process_interval(start,end)

    def outofdate(self):
        if self.alwaysrun:
            return True
        if not os.path.exists(self.outfile):
            return True
        if os.path.getmtime(self.outfile) <= os.path.getmtime(self.calfile):
            return True
        return False



config=ConfigParser()
config.read(expanduser("~/.freebusy/config"))

cdict={}
for section in config.sections():
     for k,v in config.items(section):
        cdict[k]=v


                         
page=calpage(**cdict)
if (page.outofdate()):
    page.parse()
    page.write()