def runConversionTest(self, expectedZone, icalZone): dt = datetime.datetime(2004, 10, 11, 13, 22, 21, tzinfo=icalZone) convertedZone = convertToICUtzinfo(self.view, dt).tzinfo self.failUnless(isinstance(convertedZone, (ICUtzinfo, FloatingTZ))) self.failUnlessEqual(expectedZone, convertedZone) dt = datetime.datetime(2004, 4, 11, 13, 9, 56, tzinfo=icalZone) convertedZone = convertToICUtzinfo(self.view, dt).tzinfo self.failUnless(isinstance(convertedZone, (ICUtzinfo, FloatingTZ))) self.failUnlessEqual(expectedZone, convertedZone)
def convertDatetime(dt): # coerce timezones based on coerceTzinfo if coerceTzinfo is not None: dt = TimeZone.coerceTimeZone(view, dt, coerceTzinfo) # ... and make sure we return something with an ICUtzinfo return convertToICUtzinfo(view, dt)
def deserialize_date(typeinfo, rv, text): return convertToICUtzinfo(rv, dateutilparser(text))
def whenToDatetime(rv, when): return convertToICUtzinfo(rv, dateutilparser(when))
def _importOneVObject(vobj, filters, coerceTzinfo, promptForTimezone, newItemParent): view = newItemParent.itsView itemIsNew = False newStamps = [] # by default, we'll create a new item, not change existing items itemChangeCallback = None # store up all attributes in a dictionary ... changesDict = {} # ... and define a shorthand for updating it def change(attr, value): changesDict[attr.name] = value # rruleset and userReminderInterval/userReminderTime must # be set last.... changeLast = [] # values that apply to VEVENT and VTODO ... summary = vobj.getChildValue("summary", u"") description = vobj.getChildValue("description") status = vobj.getChildValue("status", "").lower() duration = vobj.getChildValue("duration") uid = vobj.getChildValue("uid") rruleset = vobj.rruleset recurrenceID = vobj.getChildValue("recurrence_id") # ... uh, sorta completed = vobj.getChildValue("completed") def convertDatetime(dt): # coerce timezones based on coerceTzinfo if coerceTzinfo is not None: dt = TimeZone.coerceTimeZone(view, dt, coerceTzinfo) # ... and make sure we return something with an ICUtzinfo return convertToICUtzinfo(view, dt) reminderDelta = None reminderAbsoluteTime = None try: reminderValue = vobj.valarm.trigger.value except AttributeError: pass else: if type(reminderValue) is datetime.datetime: reminderAbsoluteTime = convertDatetime(reminderValue) else: assert type(reminderValue) is datetime.timedelta reminderDelta = reminderValue if vobj.name == "VEVENT": if DEBUG: logger.debug("got VEVENT %s", vobj) newStamps.append(EventStamp) dtstart = vobj.getChildValue("dtstart") if status in ("confirmed", "tentative"): pass elif status == "cancelled": # Chandler doesn't have CANCELLED status = "fyi" else: status = "confirmed" if EventStamp.transparency.name not in filters: change(EventStamp.transparency, status) location = vobj.getChildValue("location") if location: change(EventStamp.location, Calendar.Location.getLocation(view, location)) elif vobj.name == "VTODO": if DEBUG: logger.debug("got VEVENT %s", vobj) newStamps.append(TaskStamp) # VTODO with a DUE ==> EventTask due = vobj.getChildValue("due") if due is not None: newStamps.append(EventStamp) dtstart = due else: assert False, "vobj %s should always be VEVENT or VTODO" % (vobj,) # Save changes applicable to both events & tasks .... # SUMMARY <-> {EventStamp,TaskStamp}.summary if summary is not None: change(newStamps[0].summary, summary) # DESCRIPTION <-> body if description is not None: change(Note.body, description) # Absolute time reminders if reminderAbsoluteTime is not None and Remindable.reminders.name not in filters: changeLast.append(lambda item: setattr(item, Remindable.userReminderTime.name, reminderAbsoluteTime)) # Custom properties/parameters ignoredProperties = {} ignoredParameters = {} for line in vobj.lines(): name = line.name.lower() if name not in attributesUnderstood: line.transformFromNative() if not line.encoded and line.behavior: line.behavior.encode(line) ignoredProperties[name] = line.value params = u"" for key, paramvals in line.params.iteritems(): if key.lower() not in parametersUnderstood: vals = map(vobject.base.dquoteEscape, paramvals) params += ";" + key + "=" + ",".join(vals) if len(params) > 0: ignoredParameters[name] = params change(Note.icalendarProperties, ignoredProperties) change(Note.icalendarParameters, ignoredParameters) # See if we have a corresponding item already item = utility.findUID(view, uid) if item is not None: if DEBUG: logger.debug("matched UID %s with %s", uid, item) else: try: # See if uid is a valid repository UUID, if so we'll # go ahead and use it for the new item's UUID. uuid = UUID(uid) except ValueError: # Not in valid UUID format, so hash the icaluid to # generate a 16-byte string we can use for uuid uuid = UUID(md5.new(uid).digest()) logger.info("Converted icalUID '%s' to UUID '%s'", uid, str(uuid)) # If there is already an item with this UUID, use it, # otherwise we'll create one later item = view.findUUID(uuid) if item is not None: item.icalUID = unicode(uuid) if EventStamp in newStamps: dtend = vobj.getChildValue("dtend") isDate = type(dtstart) == date # 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. anyTime = getattr(dtstart, "x_osaf_anytime_param", None) == "TRUE" if duration is None: def getDifference(left, right): leftIsDate = type(left) == date rightIsDate = type(right) == date if leftIsDate: if rightIsDate: return left - right else: left = TimeZone.forceToDateTime(view, left) elif rightIsDate: right = TimeZone.forceToDateTime(view, right) return makeNaiveteMatch(view, left, right.tzinfo) - right if dtend is not None: duration = getDifference(dtend, dtstart) elif anyTime or isDate: duration = oneDay else: duration = datetime.timedelta(0) if isDate: dtstart = TimeZone.forceToDateTime(view, dtstart) # convert to Chandler's notion of all day duration duration -= oneDay elif dtstart.tzinfo is not None and promptForTimezone: # got a timezoned event, prompt (non-modally) to turn on # timezones app = wx.GetApp() if app is not None: def ShowTimezoneDialogCallback(): ShowTurnOnTimezonesDialog(view=app.UIRepositoryView) app.PostAsyncEvent(ShowTimezoneDialogCallback) promptForTimezone = False dtstart = convertDatetime(dtstart) tzinfo = dtstart.tzinfo if anyTime: change(EventStamp.anyTime, True) change(EventStamp.allDay, False) elif isDate: # allDay events should have anyTime True, so if the user # unselects allDay, the time isn't set to midnight change(EventStamp.anyTime, True) change(EventStamp.allDay, True) else: change(EventStamp.allDay, False) change(EventStamp.anyTime, False) change(EventStamp.startTime, dtstart) change(EventStamp.duration, duration) if (reminderDelta is not None) and (Remindable.reminders.name not in filters): changeLast.append(lambda item: setattr(item, EventStamp.userReminderInterval.name, reminderDelta)) if item is not None: event = EventStamp(item) if recurrenceID: if type(recurrenceID) == date: recurrenceID = datetime.datetime.combine(recurrenceID, time(tzinfo=tzinfo)) else: recurrenceID = convertToICUtzinfo(view, makeNaiveteMatch(view, recurrenceID, tzinfo)) masterEvent = EventStamp(item) event = masterEvent.getRecurrenceID(recurrenceID) if event is None and hasattr(masterEvent, "startTime"): # Some calendars, notably Oracle, serialize # recurrence-id as UTC, which wreaks havoc with # noTZ mode. So move recurrenceID to the same tzinfo # as the master's dtstart, bug 6830 masterTzinfo = masterEvent.startTime.tzinfo tweakedID = recurrenceID.astimezone(masterTzinfo) event = masterEvent.getRecurrenceID(tweakedID) if event is None: # just in case the previous didn't work tweakedID = recurrenceID.astimezone(tzinfo) event = masterEvent.getRecurrenceID(tweakedID) if event is None: # our recurrenceID didn't match an item we know # about. This may be because the item is created # by a later modification, a case we're not dealing # with. For now, just skip it. logger.info("RECURRENCE-ID '%s' didn't match rule.", recurrenceID) return (None, None, promptForTimezone) item = event.itsItem recurrenceLine = vobj.contents["recurrence-id"][0] range = recurrenceLine.params.get("RANGE", ["THIS"])[0] if range == "THISANDPRIOR": # ignore THISANDPRIOR changes for now logger.info("RECURRENCE-ID RANGE of THISANDPRIOR " "not supported") return (None, None, promptForTimezone) elif range == "THIS": itemChangeCallback = event.changeThis # check if this is a modification to a master event # if so, avoid changing the master's UUID when # creating a modification if event.getMaster() == event: mod = event._cloneEvent() mod.modificationFor = mod.occurrenceFor = event.itsItem if item.hasLocalAttributeValue(EventStamp.occurrenceFor.name): del event.occurrenceFor event = mod item = event.itsItem elif range == "THISANDFUTURE": itemChangeCallback = event.changeThisAndFuture else: logger.info("RECURRENCE-ID RANGE not recognized. " "RANGE = %s" % range) return (None, None, promptForTimezone) else: if event.rruleset is not None: # re-creating a recurring item from scratch, delete # old recurrence information # item might not be the master, though, so # get the master, or eventItem will be a deleted # event event = event.getMaster() item = event.itsItem # delete modifications the master has, to avoid # changing the master to a modification with a # different UUID for mod in itertools.imap(EventStamp, event.modifications): # [Bug 7019] # We need to del these because, in the deferred # delete case, we would have deferred items # living on, in the manner of the undead, in # master.modifications (and occurrences). This # would ultimately cause .getNextOccurrence() # to terminate prematurely. del mod.modificationFor del mod.occurrenceFor mod.itsItem.delete() event.removeRecurrence() itemChangeCallback = event.changeThis if DEBUG: logger.debug("Changing event: %s" % str(event)) assert itemChangeCallback is not None, "Must set itemChangeCallback for EventStamp imports" if rruleset is not None: # fix for Bug 6994, exdate and rdate timezones need to be # converted to ICUtzinfo instances for typ in "_rdate", "_exdate": setattr(rruleset, typ, [convertDatetime(d) for d in getattr(rruleset, typ, [])]) ruleSetItem = RecurrenceRuleSet(None, itsView=view) ruleSetItem.setRuleFromDateUtil(rruleset) changeLast.append(lambda item: setattr(item, EventStamp.rruleset.name, ruleSetItem)) if TaskStamp in newStamps: if Note._triageStatus.name not in filters: # Translate status from iCalendar to TaskStamp/ContentItem triageStatus = TriageEnum.now if status == "completed": triageStatus = TriageEnum.done elif status == "needs-action": change(Note.needsReply, True) elif status in ("", "in-process"): change(Note.needsReply, False) elif status == "cancelled": triageStatus = TriageEnum.later # @@@ Jeffrey: This may not be right... # Set triageStatus and triageStatusChanged together. if completed is not None: if type(completed) == date: completed = TimeZone.forceToDateTime(view, completed) changeLast.append(lambda item: item.setTriageStatus(triageStatus, when=completed)) itemIsNew = item is None if itemIsNew: # create a new item change(Note.icalUID, uid) kind = Note.getKind(view) item = kind.instantiateItem(None, newItemParent, uuid, withInitialValues=True) itemChangeCallback = item.__setattr__ else: if itemChangeCallback is None: itemChangeCallback = item.__setattr__ # update an existing item if rruleset is None and recurrenceID is None and EventStamp(item).rruleset is not None: # no recurrenceId or rruleset, but the existing item # may have recurrence, so delete it EventStamp(item).removeRecurrence() for attr, val in changesDict.iteritems(): # Only change a datetime if it's really different # from what the item already has: if type(val) is datetime.datetime: oldValue = getattr(item, attr, None) if oldValue is not None and oldValue == val and oldValue.tzinfo == val.tzinfo: continue itemChangeCallback(attr, val) # ... make sure the stamps are right for stamp in EventStamp, TaskStamp: if not stamp in newStamps: if has_stamp(item, stamp): stamp(item).remove() else: if not has_stamp(item, stamp): stamp(item).add() # ... and do the final set of changes for cb in changeLast: cb(item) return item, itemIsNew, promptForTimezone
def deserialize(cls, view, text, silentFailure=True): """ Parse an ICalendar blob into a list of record sets """ recordSets = {} extra = {'forceDateTriage': True} calname = None # iterate over calendars, usually only one, but more are allowed for calendar in vobject.readComponents(text, validate=False, ignoreUnreadable=True): if calname is None: calname = calendar.getChildValue('x_wr_calname') if calname is not None: extra['name'] = calname masters = {} for vobj in getattr(calendar, 'vevent_list', []): uid = vobj.getChildValue('uid') if vobj.getChildValue('recurrence_id') is None: masters[uid] = vobj uid_to_uuid_map = {} for vobj in chain(getattr(calendar, 'vevent_list', []), getattr(calendar, 'vtodo_list', [])): try: recurrenceID = vobj.getChildValue('recurrence_id') summary = vobj.getChildValue('summary', eim.NoChange) description = vobj.getChildValue('description', eim.NoChange) status = vobj.getChildValue('status', eim.NoChange) duration = vobj.getChildValue('duration') uid = vobj.getChildValue('uid') dtstart = vobj.getChildValue('dtstart') location = vobj.getChildValue('location', eim.NoChange) # bug 10821, Google serializes modifications with no master; # treat these as normal events, not modifications if not masters.get(uid): recurrenceID = None # can't just compare recurrenceID and dtstart, timezone could # have changed, and comparing floating to non-floating would # raise an exception if recurrenceID is None: dtstart_changed = True elif dtstart is None: dtstart_changed = False elif type(recurrenceID) == date or type(dtstart) == date: dtstart_changed = recurrenceID != dtstart else: dtstart_changed = (dtstart.tzinfo != recurrenceID.tzinfo or dtstart != recurrenceID) if status is not eim.NoChange: status = status.upper() start_obj = getattr(vobj, 'dtstart', None) isVtodo = (vobj.name == 'VTODO') osafStarred = vobj.getChildValue('x_osaf_starred') if osafStarred: osafStarred = (u"TRUE" == osafStarred.upper()) if dtstart is None or isVtodo: # due takes precedence over dtstart due = vobj.getChildValue('due') if due is not None: dtstart = due start_obj = getattr(vobj, 'due', None) anyTime = False if dtstart is not None: anyTimeParam = getattr(start_obj, 'x_osaf_anytime_param', '') anyTime = anyTimeParam.upper() == 'TRUE' isDate = type(dtstart) == date allDay = isDate and not anyTime emitEvent = (dtstart is not None) if duration is None: dtend = vobj.getChildValue('dtend') def getDifference(left, right): leftIsDate = (type(left) == date) rightIsDate = (type(right) == date) if leftIsDate: if rightIsDate: return left - right else: left = forceToDateTime(view, left) elif rightIsDate: right = forceToDateTime(view, right) return makeNaiveteMatch(view, left, right.tzinfo) - right if dtend is not None and dtstart is not None: duration = getDifference(dtend, dtstart) elif anyTime or isDate: duration = timedelta(1) else: duration = timedelta(0) # handle the special case of a midnight-to-midnight floating # event, treat it as allDay, bug 9579 if (not isDate and dtstart is not None and dtstart.tzinfo is None and dtstart.time() == midnight and duration.days >= 1 and duration == timedelta(duration.days)): allDay = True if isDate: dtstart = forceToDateTime(view, dtstart) # originally, duration was converted to Chandler's notion of # all day duration, but this step will be done by the # translator #duration -= oneDay if dtstart is not None: dtstart = convertToICUtzinfo(view, dtstart) dtstart = toICalendarDateTime(view, dtstart, allDay, anyTime) # convert to EIM value duration = toICalendarDuration(duration) uuid = UUIDFromICalUID(view, uid_to_uuid_map, uid) valarm = getattr(vobj, 'valarm', None) if valarm is not None: remValue = valarm.getChildValue('trigger') remDuration = valarm.getChildValue('duration') remRepeat = valarm.getChildValue('repeat') remDescription = valarm.getChildValue( 'description', "Event Reminder") trigger = None if remValue is not None: if type(remValue) is datetime: icutzinfoValue = convertToICUtzinfo(view, remValue) trigger = toICalendarDateTime( view, icutzinfoValue, False) else: assert type(remValue) is timedelta trigger = toICalendarDuration(remValue) if remDuration is not None: remDuration = toICalendarDuration(remDuration) if remRepeat is not None: remRepeat = int(remRepeat) recurrence = {} for rule_name in ('rrule', 'exrule'): rules = [] for line in vobj.contents.get(rule_name, []): rules.append(line.value) recurrence[rule_name] = (":".join(rules) if len(rules) > 0 else eim.NoChange) for date_name in ('rdate', 'exdate'): dates = [] for line in vobj.contents.get(date_name, []): dates.extend(line.value) if len(dates) > 0: if not (allDay or anyTime): dates = [ convertToICUtzinfo(view, dt) for dt in dates ] dt_value = toICalendarDateTime(view, dates, allDay, anyTime) else: dt_value = eim.NoChange recurrence[date_name] = dt_value if recurrenceID is not None: range = getattr(vobj.recurrence_id, 'range_param', 'THIS') if range != 'THIS': logger.info("Skipping a THISANDFUTURE or " "THISANDPRIOR modification") continue dateValue = allDay or anyTime recurrenceID = forceToDateTime(view, recurrenceID) recurrenceID = convertToICUtzinfo(view, recurrenceID) if recurrenceID.tzinfo != view.tzinfo.floating: recurrenceID = recurrenceID.astimezone(view.tzinfo.UTC) rec_string = translator.formatDateTime( view, recurrenceID, dateValue, dateValue) uuid += ":" + rec_string master = masters[uid] uid = eim.Inherit if (master.getChildValue('duration') == vobj.getChildValue( 'duration')): duration = eim.Inherit masterAnyTime = (getattr(master.dtstart, 'x_osaf_anytime_param', '') == 'TRUE') masterAllDay = (not masterAnyTime and type(master.dtstart.value) == date) if (masterAllDay == allDay and masterAnyTime == anyTime and not dtstart_changed): dtstart = eim.Inherit triage = eim.NoChange needsReply = eim.NoChange if isVtodo and status is not eim.NoChange: status = status.lower() code = vtodo_status_to_triage_code.get(status, "100") completed = vobj.getChildValue('completed') if completed is not None: if type(completed) == date: completed = TimeZone.forceToDateTime( view, completed) timestamp = str( Triageable.makeTriageStatusChangedTime( view, completed)) else: timestamp = getattr(vobj.status, 'x_osaf_changed_param', "0.0") auto = getattr(vobj.status, 'x_osaf_auto_param', 'FALSE') auto = ("1" if auto == 'TRUE' else "0") triage = code + " " + timestamp + " " + auto needsReply = (1 if status == 'needs-action' else 0) # VTODO's status doesn't correspond to EventRecord's status status = eim.NoChange icalExtra = eim.NoChange if not isVtodo: # not processing VTODOs icalExtra = extractUnrecognized(calendar, vobj) if icalExtra is None: icalExtra = '' else: icalExtra = icalExtra.serialize().decode('utf-8') records = [ model.NoteRecord( uuid, description, # body uid, # icalUid None, # icalProperties None, # icalParameters icalExtra, # icalExtra ), model.ItemRecord( uuid, summary, # title triage, # triage eim.NoChange, # createdOn eim.NoChange, # hasBeenSent (TODO) needsReply, # needsReply (TODO) eim.NoChange, # read ) ] if emitEvent: records.append( model.EventRecord( uuid, dtstart, duration, location, recurrence['rrule'], # rrule recurrence['exrule'], # exrule recurrence['rdate'], # rdate recurrence['exdate'], # exdate status, # status eim.NoChange # lastPastOccurrence )) if osafStarred: records.append(model.TaskRecord(uuid)) if valarm is not None: records.append( model.DisplayAlarmRecord(uuid, remDescription, trigger, remDuration, remRepeat)) else: records.append( model.DisplayAlarmRecord(uuid, None, None, None, None)) recordSets[uuid] = RecordSet(records) except vobject.base.VObjectError, e: icalendarLines = text.splitlines() logger.error( "Exception when importing icalendar, first 300 lines: \n%s" % "\n".join(icalendarLines[:300])) logger.exception( "import failed to import one event with exception: %s" % str(e)) if not silentFailure: raise
def _importOneVObject(vobj, filters, coerceTzinfo, promptForTimezone, newItemParent): view = newItemParent.itsView itemIsNew = False newStamps = [] # by default, we'll create a new item, not change existing items itemChangeCallback = None # store up all attributes in a dictionary ... changesDict = {} # ... and define a shorthand for updating it def change(attr, value): changesDict[attr.name] = value # rruleset and userReminderInterval/userReminderTime must # be set last.... changeLast = [] # values that apply to VEVENT and VTODO ... summary = vobj.getChildValue('summary', u"") description = vobj.getChildValue('description') status = vobj.getChildValue('status', "").lower() duration = vobj.getChildValue('duration') uid = vobj.getChildValue('uid') rruleset = vobj.rruleset recurrenceID = vobj.getChildValue('recurrence_id') # ... uh, sorta completed = vobj.getChildValue('completed') def convertDatetime(dt): # coerce timezones based on coerceTzinfo if coerceTzinfo is not None: dt = TimeZone.coerceTimeZone(view, dt, coerceTzinfo) # ... and make sure we return something with an ICUtzinfo return convertToICUtzinfo(view, dt) reminderDelta = None reminderAbsoluteTime = None try: reminderValue = vobj.valarm.trigger.value except AttributeError: pass else: if type(reminderValue) is datetime.datetime: reminderAbsoluteTime = convertDatetime(reminderValue) else: assert type(reminderValue) is datetime.timedelta reminderDelta = reminderValue if vobj.name == "VEVENT": if DEBUG: logger.debug("got VEVENT %s", vobj) newStamps.append(EventStamp) dtstart = vobj.getChildValue('dtstart') if status in ('confirmed', 'tentative'): pass elif status == 'cancelled': #Chandler doesn't have CANCELLED status = 'fyi' else: status = 'confirmed' if EventStamp.transparency.name not in filters: change(EventStamp.transparency, status) location = vobj.getChildValue('location') if location: change(EventStamp.location, Calendar.Location.getLocation(view, location)) elif vobj.name == "VTODO": if DEBUG: logger.debug("got VEVENT %s", vobj) newStamps.append(TaskStamp) # VTODO with a DUE ==> EventTask due = vobj.getChildValue('due') if due is not None: newStamps.append(EventStamp) dtstart = due else: assert False, "vobj %s should always be VEVENT or VTODO" % ( vobj,) # Save changes applicable to both events & tasks .... # SUMMARY <-> {EventStamp,TaskStamp}.summary if summary is not None: change(newStamps[0].summary, summary) # DESCRIPTION <-> body if description is not None: change(Note.body, description) # Absolute time reminders if (reminderAbsoluteTime is not None and Remindable.reminders.name not in filters): changeLast.append(lambda item: setattr(item, Remindable.userReminderTime.name, reminderAbsoluteTime)) # Custom properties/parameters ignoredProperties = {} ignoredParameters = {} for line in vobj.lines(): name = line.name.lower() if name not in attributesUnderstood: line.transformFromNative() if not line.encoded and line.behavior: line.behavior.encode(line) ignoredProperties[name] = line.value params=u'' for key, paramvals in line.params.iteritems(): if key.lower() not in parametersUnderstood: vals = map(vobject.base.dquoteEscape, paramvals) params += ';' + key + '=' + ','.join(vals) if len(params) > 0: ignoredParameters[name] = params change(Note.icalendarProperties, ignoredProperties) change(Note.icalendarParameters, ignoredParameters) # See if we have a corresponding item already item = utility.findUID(view, uid) if item is not None: if DEBUG: logger.debug("matched UID %s with %s", uid, item) else: try: # See if uid is a valid repository UUID, if so we'll # go ahead and use it for the new item's UUID. uuid = UUID(uid) except ValueError: # Not in valid UUID format, so hash the icaluid to # generate a 16-byte string we can use for uuid uuid = UUID(md5.new(uid).digest()) logger.info("Converted icalUID '%s' to UUID '%s'", uid, str(uuid)) # If there is already an item with this UUID, use it, # otherwise we'll create one later item = view.findUUID(uuid) if item is not None: item.icalUID = unicode(uuid) if EventStamp in newStamps: dtend = vobj.getChildValue('dtend') isDate = type(dtstart) == date # 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. anyTime = (getattr(dtstart, 'x_osaf_anytime_param', None) == 'TRUE') if duration is None: def getDifference(left, right): leftIsDate = (type(left) == date) rightIsDate = (type(right) == date) if leftIsDate: if rightIsDate: return left - right else: left = TimeZone.forceToDateTime(view, left) elif rightIsDate: right = TimeZone.forceToDateTime(view, right) return makeNaiveteMatch(view, left, right.tzinfo) - right if dtend is not None: duration = getDifference(dtend, dtstart) elif anyTime or isDate: duration = oneDay else: duration = datetime.timedelta(0) if isDate: dtstart = TimeZone.forceToDateTime(view, dtstart) # convert to Chandler's notion of all day duration duration -= oneDay elif dtstart.tzinfo is not None and promptForTimezone: # got a timezoned event, prompt (non-modally) to turn on # timezones app = wx.GetApp() if app is not None: def ShowTimezoneDialogCallback(): ShowTurnOnTimezonesDialog(view=app.UIRepositoryView) app.PostAsyncEvent(ShowTimezoneDialogCallback) promptForTimezone = False dtstart = convertDatetime(dtstart) tzinfo = dtstart.tzinfo if anyTime: change(EventStamp.anyTime, True) change(EventStamp.allDay, False) elif isDate: # allDay events should have anyTime True, so if the user # unselects allDay, the time isn't set to midnight change(EventStamp.anyTime, True) change(EventStamp.allDay, True) else: change(EventStamp.allDay, False) change(EventStamp.anyTime, False) change(EventStamp.startTime, dtstart) change(EventStamp.duration, duration) if ((reminderDelta is not None) and (Remindable.reminders.name not in filters)): changeLast.append( lambda item:setattr(item, EventStamp.userReminderInterval.name, reminderDelta)) if item is not None: event = EventStamp(item) if recurrenceID: if type(recurrenceID) == date: recurrenceID = datetime.datetime.combine( recurrenceID, time(tzinfo=tzinfo)) else: recurrenceID = convertToICUtzinfo(view, makeNaiveteMatch(view, recurrenceID, tzinfo)) masterEvent = EventStamp(item) event = masterEvent.getRecurrenceID(recurrenceID) if event is None and hasattr(masterEvent, 'startTime'): # Some calendars, notably Oracle, serialize # recurrence-id as UTC, which wreaks havoc with # noTZ mode. So move recurrenceID to the same tzinfo # as the master's dtstart, bug 6830 masterTzinfo = masterEvent.startTime.tzinfo tweakedID = recurrenceID.astimezone(masterTzinfo) event = masterEvent.getRecurrenceID(tweakedID) if event is None: # just in case the previous didn't work tweakedID = recurrenceID.astimezone(tzinfo) event = masterEvent.getRecurrenceID(tweakedID) if event is None: # our recurrenceID didn't match an item we know # about. This may be because the item is created # by a later modification, a case we're not dealing # with. For now, just skip it. logger.info("RECURRENCE-ID '%s' didn't match rule.", recurrenceID) return (None, None, promptForTimezone) item = event.itsItem recurrenceLine = vobj.contents['recurrence-id'][0] range = recurrenceLine.params.get('RANGE', ['THIS'])[0] if range == 'THISANDPRIOR': # ignore THISANDPRIOR changes for now logger.info("RECURRENCE-ID RANGE of THISANDPRIOR " \ "not supported") return (None, None, promptForTimezone) elif range == 'THIS': itemChangeCallback = event.changeThis # check if this is a modification to a master event # if so, avoid changing the master's UUID when # creating a modification if event.getMaster() == event: mod = event._cloneEvent() mod.modificationFor = mod.occurrenceFor = event.itsItem if item.hasLocalAttributeValue( EventStamp.occurrenceFor.name): del event.occurrenceFor event = mod item = event.itsItem elif range == 'THISANDFUTURE': itemChangeCallback = event.changeThisAndFuture else: logger.info("RECURRENCE-ID RANGE not recognized. " \ "RANGE = %s" % range) return (None, None, promptForTimezone) else: if event.rruleset is not None: # re-creating a recurring item from scratch, delete # old recurrence information # item might not be the master, though, so # get the master, or eventItem will be a deleted # event event = event.getMaster() item = event.itsItem # delete modifications the master has, to avoid # changing the master to a modification with a # different UUID for mod in itertools.imap(EventStamp, event.modifications): # [Bug 7019] # We need to del these because, in the deferred # delete case, we would have deferred items # living on, in the manner of the undead, in # master.modifications (and occurrences). This # would ultimately cause .getNextOccurrence() # to terminate prematurely. del mod.modificationFor del mod.occurrenceFor mod.itsItem.delete() event.removeRecurrence() itemChangeCallback = event.changeThis if DEBUG: logger.debug("Changing event: %s" % str(event)) assert itemChangeCallback is not None, \ "Must set itemChangeCallback for EventStamp imports" if rruleset is not None: # fix for Bug 6994, exdate and rdate timezones need to be # converted to ICUtzinfo instances for typ in '_rdate', '_exdate': setattr(rruleset, typ, [convertDatetime(d) for d in getattr(rruleset, typ, []) ]) ruleSetItem = RecurrenceRuleSet(None, itsView=view) ruleSetItem.setRuleFromDateUtil(rruleset) changeLast.append(lambda item: setattr(item, EventStamp.rruleset.name, ruleSetItem)) if TaskStamp in newStamps: if Note._triageStatus.name not in filters: # Translate status from iCalendar to TaskStamp/ContentItem triageStatus=TriageEnum.now if status == "completed": triageStatus = TriageEnum.done elif status == "needs-action": change(Note.needsReply, True) elif status in ("", "in-process"): change(Note.needsReply, False) elif status == "cancelled": triageStatus = TriageEnum.later # @@@ Jeffrey: This may not be right... # Set triageStatus and triageStatusChanged together. if completed is not None: if type(completed) == date: completed = TimeZone.forceToDateTime(view, completed) changeLast.append(lambda item: item.setTriageStatus(triageStatus, when=completed)) itemIsNew = (item is None) if itemIsNew: # create a new item change(Note.icalUID, uid) kind = Note.getKind(view) item = kind.instantiateItem(None, newItemParent, uuid, withInitialValues=True) itemChangeCallback = item.__setattr__ else: if itemChangeCallback is None: itemChangeCallback = item.__setattr__ # update an existing item if (rruleset is None and recurrenceID is None and EventStamp(item).rruleset is not None): # no recurrenceId or rruleset, but the existing item # may have recurrence, so delete it EventStamp(item).removeRecurrence() for attr, val in changesDict.iteritems(): # Only change a datetime if it's really different # from what the item already has: if type(val) is datetime.datetime: oldValue = getattr(item, attr, None) if (oldValue is not None and oldValue == val and oldValue.tzinfo == val.tzinfo): continue itemChangeCallback(attr, val) # ... make sure the stamps are right for stamp in EventStamp, TaskStamp: if not stamp in newStamps: if has_stamp(item, stamp): stamp(item).remove() else: if not has_stamp(item, stamp): stamp(item).add() # ... and do the final set of changes for cb in changeLast: cb(item) return item, itemIsNew, promptForTimezone
def deserialize(cls, view, text, silentFailure=True): """ Parse an ICalendar blob into a list of record sets """ recordSets = {} extra = {'forceDateTriage' : True} calname = None # iterate over calendars, usually only one, but more are allowed for calendar in vobject.readComponents(text, validate=False, ignoreUnreadable=True): if calname is None: calname = calendar.getChildValue('x_wr_calname') if calname is not None: extra['name'] = calname masters = {} for vobj in getattr(calendar, 'vevent_list', []): uid = vobj.getChildValue('uid') if vobj.getChildValue('recurrence_id') is None: masters[uid] = vobj uid_to_uuid_map = {} for vobj in chain( getattr(calendar, 'vevent_list', []), getattr(calendar, 'vtodo_list', []) ): try: recurrenceID = vobj.getChildValue('recurrence_id') summary = vobj.getChildValue('summary', eim.NoChange) description = vobj.getChildValue('description', eim.NoChange) status = vobj.getChildValue('status', eim.NoChange) duration = vobj.getChildValue('duration') uid = vobj.getChildValue('uid') dtstart = vobj.getChildValue('dtstart') location = vobj.getChildValue('location', eim.NoChange) # bug 10821, Google serializes modifications with no master; # treat these as normal events, not modifications if not masters.get(uid): recurrenceID = None # can't just compare recurrenceID and dtstart, timezone could # have changed, and comparing floating to non-floating would # raise an exception if recurrenceID is None: dtstart_changed = True elif dtstart is None: dtstart_changed = False elif type(recurrenceID) == date or type(dtstart) == date: dtstart_changed = recurrenceID != dtstart else: dtstart_changed = (dtstart.tzinfo != recurrenceID.tzinfo or dtstart != recurrenceID) if status is not eim.NoChange: status = status.upper() start_obj = getattr(vobj, 'dtstart', None) isVtodo = (vobj.name == 'VTODO') osafStarred = vobj.getChildValue('x_osaf_starred') if osafStarred: osafStarred = (u"TRUE" == osafStarred.upper()) if dtstart is None or isVtodo: # due takes precedence over dtstart due = vobj.getChildValue('due') if due is not None: dtstart = due start_obj = getattr(vobj, 'due', None) anyTime = False if dtstart is not None: anyTimeParam = getattr(start_obj, 'x_osaf_anytime_param', '') anyTime = anyTimeParam.upper() == 'TRUE' isDate = type(dtstart) == date allDay = isDate and not anyTime emitEvent = (dtstart is not None) if duration is None: dtend = vobj.getChildValue('dtend') def getDifference(left, right): leftIsDate = (type(left) == date) rightIsDate = (type(right) == date) if leftIsDate: if rightIsDate: return left - right else: left = forceToDateTime(view, left) elif rightIsDate: right = forceToDateTime(view, right) return makeNaiveteMatch(view, left, right.tzinfo) - right if dtend is not None and dtstart is not None: duration = getDifference(dtend, dtstart) elif anyTime or isDate: duration = timedelta(1) else: duration = timedelta(0) # handle the special case of a midnight-to-midnight floating # event, treat it as allDay, bug 9579 if (not isDate and dtstart is not None and dtstart.tzinfo is None and dtstart.time() == midnight and duration.days >= 1 and duration == timedelta(duration.days)): allDay = True if isDate: dtstart = forceToDateTime(view, dtstart) # originally, duration was converted to Chandler's notion of # all day duration, but this step will be done by the # translator #duration -= oneDay if dtstart is not None: dtstart = convertToICUtzinfo(view, dtstart) dtstart = toICalendarDateTime(view, dtstart, allDay, anyTime) # convert to EIM value duration = toICalendarDuration(duration) uuid = UUIDFromICalUID(view, uid_to_uuid_map, uid) valarm = getattr(vobj, 'valarm', None) if valarm is not None: remValue = valarm.getChildValue('trigger') remDuration = valarm.getChildValue('duration') remRepeat = valarm.getChildValue('repeat') remDescription = valarm.getChildValue('description', "Event Reminder") trigger = None if remValue is not None: if type(remValue) is datetime: icutzinfoValue = convertToICUtzinfo(view, remValue) trigger = toICalendarDateTime(view, icutzinfoValue, False) else: assert type(remValue) is timedelta trigger = toICalendarDuration(remValue) if remDuration is not None: remDuration = toICalendarDuration(remDuration) if remRepeat is not None: remRepeat = int(remRepeat) recurrence = {} for rule_name in ('rrule', 'exrule'): rules = [] for line in vobj.contents.get(rule_name, []): rules.append(line.value) recurrence[rule_name] = (":".join(rules) if len(rules) > 0 else eim.NoChange) for date_name in ('rdate', 'exdate'): dates = [] for line in vobj.contents.get(date_name, []): dates.extend(line.value) if len(dates) > 0: if not (allDay or anyTime): dates = [convertToICUtzinfo(view, dt) for dt in dates] dt_value = toICalendarDateTime(view, dates, allDay, anyTime) else: dt_value = eim.NoChange recurrence[date_name] = dt_value if recurrenceID is not None: range = getattr(vobj.recurrence_id, 'range_param', 'THIS') if range != 'THIS': logger.info("Skipping a THISANDFUTURE or " "THISANDPRIOR modification") continue dateValue = allDay or anyTime recurrenceID = forceToDateTime(view, recurrenceID) recurrenceID = convertToICUtzinfo(view, recurrenceID) if recurrenceID.tzinfo != view.tzinfo.floating: recurrenceID = recurrenceID.astimezone(view.tzinfo.UTC) rec_string = translator.formatDateTime(view, recurrenceID, dateValue, dateValue) uuid += ":" + rec_string master = masters[uid] uid = eim.Inherit if (master.getChildValue('duration') == vobj.getChildValue('duration')): duration = eim.Inherit masterAnyTime = (getattr(master.dtstart, 'x_osaf_anytime_param', '') == 'TRUE') masterAllDay = (not masterAnyTime and type(master.dtstart.value) == date) if (masterAllDay == allDay and masterAnyTime == anyTime and not dtstart_changed): dtstart = eim.Inherit triage = eim.NoChange needsReply = eim.NoChange if isVtodo and status is not eim.NoChange: status = status.lower() code = vtodo_status_to_triage_code.get(status, "100") completed = vobj.getChildValue('completed') if completed is not None: if type(completed) == date: completed = TimeZone.forceToDateTime(view, completed) timestamp = str(Triageable.makeTriageStatusChangedTime(view, completed)) else: timestamp = getattr(vobj.status, 'x_osaf_changed_param', "0.0") auto = getattr(vobj.status, 'x_osaf_auto_param', 'FALSE') auto = ("1" if auto == 'TRUE' else "0") triage = code + " " + timestamp + " " + auto needsReply = (1 if status == 'needs-action' else 0) # VTODO's status doesn't correspond to EventRecord's status status = eim.NoChange icalExtra = eim.NoChange if not isVtodo: # not processing VTODOs icalExtra = extractUnrecognized(calendar, vobj) if icalExtra is None: icalExtra = '' else: icalExtra = icalExtra.serialize().decode('utf-8') records = [model.NoteRecord(uuid, description, # body uid, # icalUid None, # icalProperties None, # icalParameters icalExtra, # icalExtra ), model.ItemRecord(uuid, summary, # title triage, # triage eim.NoChange, # createdOn eim.NoChange, # hasBeenSent (TODO) needsReply, # needsReply (TODO) eim.NoChange, # read )] if emitEvent: records.append(model.EventRecord(uuid, dtstart, duration, location, recurrence['rrule'], # rrule recurrence['exrule'], # exrule recurrence['rdate'], # rdate recurrence['exdate'], # exdate status, # status eim.NoChange # lastPastOccurrence )) if osafStarred: records.append(model.TaskRecord(uuid)) if valarm is not None: records.append( model.DisplayAlarmRecord( uuid, remDescription, trigger, remDuration, remRepeat )) else: records.append( model.DisplayAlarmRecord( uuid, None, None, None, None)) recordSets[uuid] = RecordSet(records) except vobject.base.VObjectError, e: icalendarLines = text.splitlines() logger.error("Exception when importing icalendar, first 300 lines: \n%s" % "\n".join(icalendarLines[:300])) logger.exception("import failed to import one event with exception: %s" % str(e)) if not silentFailure: raise
def date_parse(view, s): """Parse using dateutil's parse, then convert to ICUtzinfo timezones.""" return convertToICUtzinfo(view, dateutil_parse(s))