Commit by: jeffrey Modified files: chandler/parcels/osaf/framework/sharing/ICalendar.py 1.2 1.3 chandler/parcels/osaf/views/main/Main.py 1.104 1.105 chandler/parcels/osaf/framework/sharing/tests/TestImportICalendar.py 1.4 1.5
Log message: - Improved ICalendarFormat's import code to use timezones and recurrence - Changed Import menu to use ICalendarFormat - Added a bit to the ImportICalendar test case - Got exportProcess working for ICalendarFormat ViewCVS links: http://cvs.osafoundation.org/index.cgi/chandler/parcels/osaf/framework/sharing/ICalendar.py.diff?r1=text&tr1=1.2&r2=text&tr2=1.3 http://cvs.osafoundation.org/index.cgi/chandler/parcels/osaf/views/main/Main.py.diff?r1=text&tr1=1.104&r2=text&tr2=1.105 http://cvs.osafoundation.org/index.cgi/chandler/parcels/osaf/framework/sharing/tests/TestImportICalendar.py.diff?r1=text&tr1=1.4&r2=text&tr2=1.5 Index: chandler/parcels/osaf/framework/sharing/ICalendar.py diff -u chandler/parcels/osaf/framework/sharing/ICalendar.py:1.2 chandler/parcels/osaf/framework/sharing/ICalendar.py:1.3 --- chandler/parcels/osaf/framework/sharing/ICalendar.py:1.2 Thu Jan 20 00:49:34 2005 +++ chandler/parcels/osaf/framework/sharing/ICalendar.py Sun Jan 23 22:55:32 2005 @@ -7,16 +7,87 @@ import vobject import logging import mx +import dateutil.tz +import datetime +import itertools logger = logging.getLogger('ICalendar') logger.setLevel(logging.INFO) +localtime = dateutil.tz.tzlocal() +utc = dateutil.tz.tzutc() + +MAXRECUR = 10 + +def convertToMX(dt, tz=None): + """Convert the given datetime into an mxDateTime. + + Convert dt to tz if it has a timezone. tz defaults to localtime. + + >>> import datetime, mx.DateTime + >>> dt = datetime.datetime(2004, 12, 20, 18, tzinfo=utc) + >>> mxdt = convertToMX(dt, pacific) + >>> print mxdt + 2004-12-20 10:00:00.00 + >>> type(mxdt) + <type 'DateTime'> + + """ + if not tz: tz = localtime + if getattr(dt, 'tzinfo', None): dt = dt.astimezone(tz) + return mx.DateTime.mktime(dt.timetuple()) + +def convertToUTC(dt, tz = None): + """Convert the given mxDateTime (without tz) into datetime with tzinfo=UTC. + + >>> import datetime, mx.DateTime + >>> mxdt = mx.DateTime.DateTime(2004, 12, 20, 12) + >>> dt = convertToUTC(mxdt, pacific) + >>> print dt + 2004-12-20 20:00:00+00:00 + + """ + if not tz: tz = localtime + args = (dt.year, dt.month, dt.day, dt.hour, dt.minute, int(dt.second)) + return datetime.datetime(*args).replace(tzinfo=tz).astimezone(utc) + +def eventsToVObject(items, cal=None): + """Iterate through items, add to cal, create a new vcalendar if needed. + + Chandler doesn't do recurrence yet, so for now we don't worry + about timezones. + + """ + if cal is None: + cal = vobject.iCalendar() + for event in items: + vevent = cal.add('vevent') + vevent.add('uid').value = unicode(event.itsUUID) + try: + vevent.add('summary').value = event.displayName + except AttributeError: + pass + try: + vevent.add('dtstart').value = convertToUTC(event.startTime) + except AttributeError: + pass + try: + vevent.add('dtend').value = convertToUTC(event.endTime) + except AttributeError: + pass + try: + vevent.add('description').value = event.body.getReader().read() + except AttributeError: + pass + return cal + class ICalendarFormat(Sharing.ImportExportFormat): myKindID = None myKindPath = "//parcels/osaf/framework/sharing/ICalendarFormat" __calendarEventPath = "//parcels/osaf/contentmodel/calendar/CalendarEvent" - + __lobPath = "//Schema/Core/Lob" + def fileStyle(self): return self.STYLE_SINGLE @@ -44,12 +115,13 @@ #@@@MOR Raise something # @@@MOR Total hack - newtext = [] - for c in text: - if ord(c) > 127: - c = " " - newtext.append(c) - text = "".join(newtext) + # this shouldn't be necessary anymore + #newtext = [] + #for c in text: + # if ord(c) > 127: + # c = " " + # newtext.append(c) + #text = "".join(newtext) input = StringIO.StringIO(text) calendar = vobject.readComponents(input, validate=True).next() @@ -57,51 +129,108 @@ countNew = 0 countUpdated = 0 eventKind = self.itsView.findPath(self.__calendarEventPath) - + textKind = self.itsView.findPath(self.__lobPath) + for event in calendar.vevent: # See if we have a corresponding item already, or create one uuid = UUID(event.uid[0].value[:36]) # @@@MOR, stripping "-RID" - eventItem = self.itsView.findUUID(uuid) - if eventItem is None: - # @@@MOR This needs to use the new defaultParent framework - # to determine the parent - eventItem = application.Parcel.NewItem(self.itsView, - None, self.itsView.findPath("//userdata"), - eventKind, uuid) - countNew += 1 - else: - countUpdated += 1 + # FIXME Why are we stripping to 36 characters? + + # hack until recurrence set can be stored in Chandler with one UUID + # as it's modeled by iCalendar + uuidMatchItem = self.itsView.findUUID(uuid) + + # For now we'll expand recurrence sets, first find attributes that + # will be constant across the recurrence set. try: - eventItem.displayName = event.summary[0].value + displayName = event.summary[0].value except AttributeError: - eventItem.displayName = "" + displayName = "" try: - eventItem.description = event.description[0].value - # print "Has a description:", eventItem.description + description = event.description[0].value except AttributeError: - eventItem.description = "" - - dt = event.dtstart[0].value - eventItem.startTime = \ - mx.DateTime.ISO.ParseDateTime(dt.isoformat()) + description = None + # Commented out because VALARM won't work until vobject is updated + #try: + # # FIXME assumes DURATION, not DATE-TIME + # reminderDelta = test[0].trigger[0].value + #except AttributeError: + # reminderDelta = None + + # RFC2445 allows VEVENTs without DTSTART, but it's hard to guess + # what that would mean, so we won't catch an exception if there's no + # dtstart. + dtstart = event.dtstart[0].value + try: - dt = event.dtend[0].value - eventItem.endTime = \ - mx.DateTime.ISO.ParseDateTime(dt.isoformat()) - except: - eventItem.duration = mx.DateTime.DateTimeDelta(0, 1) - - item.add(eventItem) - # print "Imported", eventItem.displayName, eventItem.startTime, - # eventItem.duration, eventItem.endTime + duration = event.duration[0].value + except AttributeError: + # note that duration = dtend - dtstart isn't strictly correct + # throughout a recurrence set, 1 hour differences might happen + # around DST, but we'll ignore that corner case for now + try: + duration = event.dtend[0].value - dtstart + # FIXME no end time or duration, Chandler's UI doesn't seem to + # like events with no duration, so for now we'll set a dummy + # duration of 1 hour + except AttributeError: + duration = datetime.timedelta(hours=1) + # Iterate through recurrence set. Infinite recurrence sets are + # common, something has to be done to avoid infinite loops. + # We'll arbitrarily limit ourselves to MAXRECUR recurrences. + first = True + for dt in itertools.islice(event.rruleset, MAXRECUR): + # Hack to deal with recurrence set having a single UID but + # needing to become multiple items with distinct UUIDs. For the + # first item, use the right UUID (and the matching Item if it + # exists), for later items, create a new uuid. + if first and uuidMatchItem is not None: + eventItem = uuidMatchItem + countUpdated += 1 + else: + if not first: + uuid = UUID() + # @@@MOR This needs to use the new defaultParent framework + # to determine the parent + eventItem = application.Parcel.NewItem(self.itsView, + None, self.itsView.findPath("//userdata"), + eventKind, uuid) + countNew += 1 + + eventItem.displayName = displayName + eventItem.startTime = convertToMX(dt) + eventItem.endTime = convertToMX(dt + duration) + + # I think Item.description describes a Kind, not userdata, so + # I'm using DESCRIPTION <-> body + if description is not None: + eventItem.body = textKind.makeValue(description) + + # Commented out because VALARM won't work until vobject is updated + #if reminderTime is not None: + # eventItem.reminderTime = convertToMX(dt + reminderDelta) + + item.add(eventItem) + first = False + logger.debug("Imported %s %s %s" % (eventItem.displayName, + eventItem.startTime, eventItem.endTime)) + logger.info("...iCalendar import of %d new items, %d updated" % \ (countNew, countUpdated)) return item def exportProcess(self, item, depth=0): - # item is the whole collection - pass + # item is the whole collection or it may be a single event + if isinstance(item, ItemCollection.ItemCollection): + events = [item] + else: + events = item.contents + + cal = eventsToVObject(events) + return cal.serialize() + + \ No newline at end of file Index: chandler/parcels/osaf/framework/sharing/tests/TestImportICalendar.py diff -u chandler/parcels/osaf/framework/sharing/tests/TestImportICalendar.py:1.4 chandler/parcels/osaf/framework/sharing/tests/TestImportICalendar.py:1.5 --- chandler/parcels/osaf/framework/sharing/tests/TestImportICalendar.py:1.4 Wed Jan 19 15:52:33 2005 +++ chandler/parcels/osaf/framework/sharing/tests/TestImportICalendar.py Sun Jan 23 22:55:34 2005 @@ -1,8 +1,8 @@ """ A helper class which sets up and tears down dual RamDB repositories """ -__revision__ = "$Revision: 1.4 $" -__date__ = "$Date: 2005/01/19 23:52:33 $" +__revision__ = "$Revision: 1.5 $" +__date__ = "$Date: 2005/01/24 06:55:34 $" __copyright__ = "Copyright (c) 2003-2004 Open Source Applications Foundation" __license__ = "http://osafoundation.org/Chandler_0.1_license_terms.htm" @@ -78,6 +78,12 @@ self.share.get() # @@@ Put some checking of the imported items here + + event=self.repo.findUUID('BED962E5-6042-11D9-BE74-000A95BB2738') + self.assert_(event.displayName == u'3 hour event', + "SUMMARY of first VEVENT not imported correctly, displayName is %s" + % event.displayName) + # Also, put in a test of updating from a modified ics file. Index: chandler/parcels/osaf/views/main/Main.py diff -u chandler/parcels/osaf/views/main/Main.py:1.104 chandler/parcels/osaf/views/main/Main.py:1.105 --- chandler/parcels/osaf/views/main/Main.py:1.104 Sun Jan 23 14:42:54 2005 +++ chandler/parcels/osaf/views/main/Main.py Sun Jan 23 22:55:33 2005 @@ -1,5 +1,5 @@ -__version__ = "$Revision: 1.104 $" -__date__ = "$Date: 2005/01/23 22:42:54 $" +__version__ = "$Revision: 1.105 $" +__date__ = "$Date: 2005/01/24 06:55:33 $" __copyright__ = "Copyright (c) 2004 Open Source Applications Foundation" __license__ = "http://osafoundation.org/Chandler_0.1_license_terms.htm" @@ -30,6 +30,7 @@ from osaf.framework.blocks.Block import Block from osaf.contentmodel.ItemCollection import ItemCollection import osaf.framework.utils.imports.icalendar as ical +import osaf.framework.sharing.ICalendar as ICalendar class MainView(View): """ @@ -298,16 +299,27 @@ def onImportIcalendarEvent(self, event): # triggered from "Test | Import iCalendar" Menu - repository = Globals.repository - self.setStatusMessage ("Importing from " + ical.INFILE) + rep = Globals.repository + parent = rep.findPath("//userdata/contentitems") + + self.setStatusMessage ("Importing from import.ics") try: - if ical.importFile(ical.INFILE, repository): - self.setStatusMessage ("Import completed") - else: - repository.logger.info("Failed importFile") - self.setStatusMessage("Import failed") + conduit = rep.findPath("//userdata/fsconduit") + if conduit is None: + conduit = Sharing.FileSystemConduit(name="fsconduit", + parent=parent, sharePath=".", shareName="import.ics") + format = rep.findPath("//userdata/icalImportFormat") + if format is None: + format = ICalendar.ICalendarFormat(name="icalImportFormat", + parent=parent) + share = rep.findPath("//userdata/icalImportShare") + if share is None: + share = Sharing.Share(name="icalImportShare", parent=parent, + conduit=conduit, format=format) + share.get() + self.setStatusMessage ("Import completed") except Exception, e: - repository.logger.info("Failed importFile, caught exception " + str(e)) + rep.logger.info("Failed importFile, caught exception " + str(e)) self.setStatusMessage("Import failed") def onExportIcalendarEvent(self, event): _______________________________________________ Commits mailing list Commits@osafoundation.org http://lists.osafoundation.org/mailman/listinfo/commits