#!/usr/bin/python """ iCalendar 2.0 Implementation as described in RFC 2445 We use the former name 'vCalendar' here. WARNING: This iCalendar implementation is VERY incomplete and should not be used for any real-world calendar applications! """ # Copyright (C) 2004 Henning Jacobs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # $Id: vcalendar.py 82 2004-07-11 13:01:44Z henning $ import re, time from types import * import string import debug from vcore import * import Set FIELDNAMES = [ "Summary", "Description", "Categories", "Comment", "Location", "Attendee", # special PyCoCuMa virtual field: "AttendeeIds", "Organizer", "Contact", "DateStart", "DateEnd", "LastModification", "Created", "TimeStamp", "URL", "UID"] FIELDNAME2ATTR = { "Summary" : "summary", "Description" : "description", "Categories" : "categories", "Comment" : "comment", "Location" : "location", "Attendee" : "attendee", "AttendeeIds" : "getAttendeeIds", "Organizer" : "organizer", "Contact" : "contact", "DateStart" : "dtstart", "DateEnd" : "dtend", "LastModification": "last_mod", "Created" : "created", "TimeStamp" : "dtstamp", "URL" : "url", "UID" : "uid" } SUPPORTED_VERSIONS = ["2.0"] class vC_caladdress(vC_text): def __init__(self, value="", params=None): parts = value.split(':') if len(parts) > 1: value = ':'.join(parts[1:]) vC_text.__init__(self, value, params) # PyCoCuMa's Contact UID is wrapped in the DIR URI Param: # E.g: http://pycocuma.localhost:8810/xmlrpc/vCard?UID=wxyz-1234-56 uid = self.params.get('dir') if uid is None: self._uid = '' else: parts = uid[0].split('=') self._uid = parts[-1] def getUID(self): return self._uid def setUID(self, uid): self._uid = uid def assignFromCard(self, card): "We set our Calendar Address from a vCard" fn = card.fn.get() if card.email: # Find the preferred Email-Address: prefmail = 0 for i in range(len(card.email)): if card.email[i].is_pref(): prefmail = i self.set(card.email[prefmail].get()) else: # Sorry, the card has no email: self.set("nobody@nowhere") # Set our Common Name to the card's FormattedName: self.params.set('cn', Set.Set(fn)) # Set our UID: if not card.uid.is_empty(): self.setUID(card.uid.get()) else: self.setUID('') def is_pref(self): # This Function is required by InputWidgets.MultiRecordEdit return False def VCF_repr(self): if self._uid: # This is a fake URI to wrap the vCard's UID: dir = "http://pycocuma.localhost:8810/xmlrpc/vCard?UID=%s" % self._uid self.params.set('dir', Set.Set(dir)) return self.params.VCF_repr() + ":MAILTO:" + self.value.VCF_repr() class vC_attendee(vC_caladdress): "Event Attendee" def __init__(self, value="", params=None): vC_caladdress.__init__(self, value, params) class vC_organizer(vC_caladdress): "Event Organizer" def __init__(self, value="", params=None): vC_caladdress.__init__(self, value, params) # Stores information about last call to getFieldValue(): # _lastreturnedfield = "%d_%s" % (vevent.handle(), fieldname) _lastreturnedfield = "" _lastreturnedvalueidx = 0 _uidcounter = 0 class vEvent: """vEvent Implementation as described in RFC 2445""" def _generateUID(self): global _uidcounter dtstr = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()) _uidcounter += 1 return "%s-%04X@pycocuma.srcco.de" % (dtstr, _uidcounter) def _name2attr(self, name): return name.lower().replace("-", "_") def _attr2name(self, attr): return attr.upper().replace("_", "-") def __init__(self, block_of_lines=[]): self.__dict__["_handle"] = None # default values: self.__dict__["data"] = { "summary" : vC_text(), "description": vC_text(), "categories": vC_categories(), "comment" : vC_text(), "location" : vC_text(), "attendee" : [], "organizer" : vC_organizer(), "contact" : vC_text(), "dtstart" : vC_datetime(), "dtend" : vC_datetime(), "last_mod" : vC_datetime(time.gmtime()), "created" : vC_datetime(time.gmtime()), "dtstamp" : vC_datetime(time.gmtime()), "url" : vC_text(), # RFC-2445 says that UID MUST BE present: "uid" : vC_text(self._generateUID())} # Name to function mapping: self.__dict__["name_func"] = { "SUMMARY" : vC_text, "DESCRIPTION": vC_text, "CATEGORIES": vC_categories, "COMMENT" : vC_text, "LOCATION" : vC_text, "ATTENDEE" : vC_attendee, "ORGANIZER" : vC_organizer, "CONTACT" : vC_text, "DTSTART" : vC_datetime, "DTEND" : vC_datetime, "LAST-MOD" : vC_datetime, "CREATED" : vC_datetime, # Timestamp will only be set once on object creation: "DTSTAMP" : vC_datetime, "URL" : vC_text, "UID" : vC_text} if type(block_of_lines) != ListType: # Input was a string? # Delete empty lines: block_of_lines = filter(isNonEmptyLine, block_of_lines.split('\n')) # de-fold: makeloglines(block_of_lines) block_of_lines = map(vC_contentline, block_of_lines) for line in block_of_lines: self._insertline(line) global _lastreturnedvalueidx _lastreturnedvalueidx = 0 def _insertline(self, line): if self.name_func.has_key(line.name): attr = self._name2attr(line.name) if hasattr(self, attr) and type(self.__getattr__(attr)) == ListType: # Multi-Value: self.__getattr__(attr).append(\ self.name_func[line.name](line.value, line.params)) else: # Single-Value: self.__setattr__(attr,\ self.name_func[line.name](line.value, line.params)) def __getattr__(self, name): try: return self.data[name] except: raise AttributeError, "No Attribute '%s'" % (name) def __setattr__(self, name, value): try: self.data[name] = value except: raise AttributeError, "No Attribute '%s'" % (name) def __repr__(self): ret = "<%s: " % self.__class__ for key, value in zip(self.data.keys(), self.data.values()): ret = ret + "%s: %s\n" % (repr(key), repr(value)) return ret+">" def handle(self): return self.__dict__["_handle"] def sethandle(self, handle): "Handle can be set only once in a lifetime!" if self.__dict__["_handle"] == None: self.__dict__["_handle"] = handle def getAttendeeIds(self): "Return comma separated list of attendee UIDs" return ', '.join(map(vC_caladdress.getUID, self.attendee)) def getFieldValue(self, field_and_idx): """Returns content of field as vC_value field_and_idx => e.g. 'Street 1' return => vC_value or None On multiple calls it returns further items from value array (Call this functions until it returns None)""" global _lastreturnedfield, _lastreturnedvalueidx def lastretfieldstr(field, self=self): handle = self.handle() if handle is None: return "None_%s" % (field) else: return "%d_%s" % (handle, field) parts = field_and_idx.split(" ") field = parts[0] try: idx = int(parts[1])-1 except: if lastretfieldstr(field) == _lastreturnedfield: idx = _lastreturnedvalueidx + 1 else: idx = 0 attr = FIELDNAME2ATTR[field] attrobj = getsubattr(self, attr) if type(attrobj) == ListType: if len(attrobj) > idx: value = attrobj[idx] else: value = None elif idx >0: value = None else: value = attrobj _lastreturnedvalueidx = idx _lastreturnedfield = lastretfieldstr(field) return value def getFieldValueStr(self, field_and_idx, default=""): """Returns content of field as string field_and_idx => e.g. 'Street 1' return => e.g. 'Fifth-Avenue 89'""" val = self.getFieldValue(field_and_idx) if val: return flattenattr(val) else: # the field does not exist # (NOTE: this does not mean empty fields!): return default def VCF_repr(self): ret = "BEGIN:VEVENT\n" Dict = self.data for name, val in Dict.items(): if type(val) == ListType: for itm in val: ret = ret + log2phylines(self._attr2name(name) + itm.VCF_repr() + "\n") elif val != None: if not val.is_empty(): ret = ret + log2phylines(self._attr2name(name) + val.VCF_repr() + "\n") return ret + "END:VEVENT\n" class vCalendar: """List of vEvents""" def __init__(self): self.handlecounter = 0 self.data = {} def sortedlist(self, sortby=""): "Return list of event handles" if not sortby: sortby = "DateStart" events = self.data.values() handles = self.data.keys() def getfieldvaluestr(event, field=sortby): return event.getFieldValueStr(field) decorated = zip(map(getfieldvaluestr, events), handles) decorated.sort() self.forgetLastReturnedField() return [ handle for sortstr, handle in decorated ] def LoadFromFile(self, fname): "Load from *.ics file" try: fd = open(fname, "rb") self.LoadFromStream(fd) fd.close() except: # Loading failed return False return True def LoadFromStream(self, stream, encoding='utf8'): "Load from any text stream with file-like methods" vEventBlock = None lines = stream.readlines() makeloglines(lines) for line in filter(isNonEmptyLine, lines): if type(line) != UnicodeType: line = unicode(line, encoding, 'replace') line = vC_contentline(line) if line.name == "BEGIN": if line.value.upper() == "VEVENT": vEventBlock = [] if vEventBlock is not None: vEventBlock.append(line) if line.name == "END" \ and line.value.upper() == "VEVENT": self.add(vEvent(vEventBlock)) def SaveToFile(self, fname): "Save to *.ics file" fd = open(fname, "wb") self.SaveToStream(fd) fd.close() def SaveToStream(self, stream, encoding='utf8'): "Save to Stream (FileDescriptor)" stream.write(self.VCF_repr().encode(encoding, 'replace')) def __str__(self): return str(zip(self.data.keys(), map(str,self.data.values()))) def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def clear(self): "Removes all vEvents" self.data.clear() def add(self, event=None): "Add new vEvent to list" if event == None: event = vEvent() self.handlecounter += 1 newhandle = self.handlecounter # Handle must not have been set before: event.sethandle(newhandle) self.data[newhandle] = event return newhandle def delete(self, handle): "Removes vEvent with handle from list" if self.data.has_key(handle): del self.data[handle] return True else: return False def forgetLastReturnedField(self): "Reset the lastreturnedfield variable" global _lastreturnedfield, _lastreturnedvalueidx _lastreturnedfield = "" _lastreturnedvalueidx = 0 def VCF_repr(self): "Representation for file output" from __version__ import __version__ ret = "BEGIN:VCALENDAR\nVERSION:2.0\n" ret = ret + "PRODID:-//Henning Jacobs//NONSGML PyCoCuMa Calendar Version %s//EN\n\n" % __version__ for event in self.data.values(): ret = ret + event.VCF_repr() + "\n" # add blank line between events return ret+"END:VCALENDAR\n"