def parseEventInfo(mailStamp): assert isinstance(mailStamp, MailStamp) if has_stamp(mailStamp.itsItem, EventStamp): # The message has been stamped as # an event which means its event info has # already been populated return eventStamp = EventStamp(mailStamp.itsItem) eventStamp.add() # This uses the default Chandler locale determined from # the OS or the command line flag --locale (-l) startTime, endTime, countFlag, typeFlag = \ _parseEventInfoForLocale(mailStamp) #XXX the parsedatetime API does not always return the # correct parsing results. # # Further investigation needs to be done. # What I would like to do is try in the user's current # locale then try in English. But in my testing the # parseText API returns a positive count flag when # if the text contains date time info that does # not match the passed locale. The value of the # startTime and endTime wil be the current users # localtime which is not correct. # # I also see incorrect results when text contains # a start and end date. As well as when the # text contains localized times such as 4pm. # In some instances it does correctly parse the time # in others it does not. # # The English parsing fallback is commented out till # the above issues are rosolved. # #if countFlag == 0 and not getLocale().startswith(u"en"): # # Lets try using English language date parsing rules # # as a fallback. # startTime, endTime, countFlag, typeFlag = \ # _parseEventInfoForLocale(messageObject, "en") if countFlag == 0: # No datetime info found in either the mail message subject # or the mail message body so do not set any event date time info. return setEventDateTime(mailStamp.itsItem, startTime, endTime, typeFlag)
def onSetContentsEvent(self, event): """ Here would be a good place to make sure that items selected in the old contents are also selected in the new contents. """ contents = event.arguments['item'] # collectionList.first() is the currently selected collection in # the sidebar contentsCollection = contents.collectionList.first() self.setContentsOnBlock(contents, contentsCollection) # bug 5613, when a new collection is selected, items in overlaid # collections should be unselected selection = self.GetSelection() # Bug 5817, the iterable returned by iterSelection will complain if an # item is removed, so create a (seemingly useless) list before iterating for item in list(selection.iterSelection()): if (EventStamp(item).getMaster().itsItem not in self.contentsCollection): selection.unselectItem(item) # Posting select items event will display the correct item in the detail view. self.postSelectItemsBroadcast() self.synchronizeWidget()
def OnSelectItem(self, item): """ Called when an item is hit, to select the item. Subclasses can override to handle item selection. """ selection = self.blockItem.GetSelection() # usually there's no need to call Refresh, postSelectItemsBroadcast will # cause notifications to force a redraw. But if selection was empty # from clicking outside a lozenge, the selected item didn't # change in the rest of the system. In that case nothing changes # in the repository, so do a redraw. shouldRefresh = selection.isSelectionEmpty() if item: selection.setSelectionToItem(item) shouldRefresh |= (getattr(item, 'inheritFrom', None) is not None and EventStamp(item).modificationFor is None) else: selection.clearSelection() self.blockItem.postSelectItemsBroadcast() if shouldRefresh: self.Refresh()
def findClusters(self, toSend): """ Return a list of tuples of (alias, deleteFlag) pairs, clustering recordsets that need to be serialized together (recurrence modifications and masters). The first pair will be the master. For instance: [((master1, False), (mod1, False)), ((master2, False),)] """ mastersChanged = set() mastersDeleted = set() view = self.itsView translator = self.translator(self.itsView) for alias, rs in toSend.iteritems(): masterAlias, recurrenceID = splitUUID(view, alias) s = mastersChanged if (rs is not None or recurrenceID) else mastersDeleted s.add(masterAlias) mastersChanged = mastersChanged - mastersDeleted clusters = [((alias, True), ) for alias in mastersDeleted] for masterAlias in mastersChanged: cluster = [(masterAlias, False)] clusters.append(cluster) master = view.findUUID(masterAlias) for mod in getattr(EventStamp(master), 'modifications', []): cluster.append((translator.getAliasForItem(mod), False)) return clusters
def AddItems(self, itemList): """ Override this to add the dropped items to your widget. """ if self.hoverDate is not None: for item in itemList: proxy = RecurrenceDialog.getProxy(u"ui", item, cancelCallback=self.wxSynchronizeWidget) event = EventStamp(proxy) if not has_stamp(proxy, EventStamp): event.add() # stamp as an event event.anyTime = True oldTime = getattr(event, "startTime", self.hoverDate).timetz() event.startTime = datetime.combine(self.hoverDate, oldTime) proxy.setTriageStatus("auto") self.hoverDate = None self.Refresh()
def addEventStamp(item, recur=False): es = EventStamp(item) es.add() es.summary = uw("Test Event Summary") tzinfo = item.itsView.tzinfo.floating # Choose random days, hours startDelta = timedelta(days=random.randint(0, 30), hours=random.randint(0, 24)) now = datetime.now(tzinfo) closeToNow = datetime(now.year, now.month, now.day, now.hour, int(now.minute / 30) * 30, tzinfo=now.tzinfo) es.startTime = closeToNow + startDelta es.anyTime = True # Choose random minutes es.duration = timedelta(minutes=60) es.location = Calendar.Location.getLocation(view, uw("My House")) es.itsItem.importance = random.choice(pim.ImportanceEnum.values) es.itsItem.setTriageStatus(randomEnum(pim.TriageEnum)) if recur: rule = RecurrenceRule(itsView=view) rule.freq = 'daily' rule.until = datetime(2008, 9, 14, 19, tzinfo=view.tzinfo.default) rule.untilIsDate = False ruleSet = RecurrenceRuleSet(itsView=view) ruleSet.addRule(rule) es.rruleset = ruleSet return es
def __init__(self, bounds, itemOrEvent): """ @param bounds: the bounds of the item as drawn on the canvas. @type bounds: wx.Rect @param item: the item drawn on the canvas in these bounds @type itemOrEvent: C{Item} or C{EventStamp} """ # @@@ scaffolding: resize bounds is the lower 5 pixels self._bounds = bounds if isinstance(itemOrEvent, EventStamp): self.event = itemOrEvent else: self.event = EventStamp(itemOrEvent)
def addEventStamp(item, recur=False): es = EventStamp(item) es.add() es.summary = uw("Test Event Summary") tzinfo = view.tzinfo.floating # Choose random days, hours startDelta = timedelta(days=random.randint(0, 30), hours=random.randint(0, 24)) now = datetime.now(tzinfo) closeToNow = datetime(now.year, now.month, now.day, now.hour, int(now.minute/30) * 30, tzinfo=now.tzinfo) es.startTime = closeToNow + startDelta es.anyTime = True # Choose random minutes es.duration = timedelta(minutes=60) es.location = Calendar.Location.getLocation(view, uw("My House")) es.itsItem.importance = random.choice(pim.ImportanceEnum.values) es.itsItem.setTriageStatus(randomEnum(pim.TriageEnum)) if recur: rule = RecurrenceRule(itsView=view) rule.freq = 'daily' rule.until = datetime(2008, 9, 14, 19, tzinfo=view.tzinfo.default) rule.untilIsDate = False ruleSet = RecurrenceRuleSet(itsView=view) ruleSet.addRule(rule) es.rruleset = ruleSet return es
def AddItems(self, itemList): """ Override this to add the dropped items to your widget. """ if self.hoverDate is not None: for item in itemList: proxy = RecurrenceDialog.getProxy( u'ui', item, cancelCallback=self.wxSynchronizeWidget) event = EventStamp(proxy) if not has_stamp(proxy, EventStamp): event.add() # stamp as an event event.anyTime = True oldTime = getattr(event, 'startTime', self.hoverDate).timetz() event.startTime = datetime.combine(self.hoverDate, oldTime) proxy.setTriageStatus('auto') self.hoverDate = None self.Refresh()
def startTest(self): def mondayPlus(inc=0): """return a m/d/yy date string equal to this Monday plus inc days""" today = datetime.date.today() daysUntilMonday = today.weekday() if daysUntilMonday == 6: daysUntilMonday = -1 #sunday is special case monday = today - datetime.timedelta(days=daysUntilMonday) incDay = monday + datetime.timedelta(days=inc) view = wx.GetApp().UIRepositoryView value = datetime.datetime.combine(incDay, datetime.time(0, tzinfo=view.tzinfo.default)) dateStr = shortDateFormat.format(view, value) return dateStr # resize the Chandler window to (1024,720): this test sort of crumble if the window is too small frame = wx.GetApp().mainFrame frame.SetSize((1024,720)) # switch to calendar view testView = QAUITestAppLib.UITestView(self.logger) testView.SwitchToCalView() # make user collection, since only user # collections can be displayed as a calendar col = QAUITestAppLib.UITestItem("Collection", self.logger) evtDate = mondayPlus() evtSecondDate = mondayPlus(1) evtThirdDate = mondayPlus(2) evtRecurrenceEnd = mondayPlus(365) evtNextWeek = mondayPlus(7) # Make sure we're not showing timezones now (we'll put it back below) tzPrefs = schema.ns('osaf.pim', QAUITestAppLib.App_ns.itsView).TimezonePrefs oldTZPref = tzPrefs.showUI tzPrefs.showUI = False # Create a vanilla event; leave the timezone alone so we can make sure # it's floating. event = QAUITestAppLib.UITestItem("Event", self.logger) event.SetAttr(displayName=uw("Birthday Party"), startDate=evtDate, startTime="6:00 PM", location=uw("Club101"), status="FYI", body=uw("This is a birthday party invitation")) # Check a few things: that those attributes got set right, plus # a few defaulty things worked (timezone, endtime) event.CheckDisplayedValues("Checking initial setup", HeadlineBlock=(True, uw("Birthday Party")), EditAllDay=(True, False), EditCalendarStartDate=(True, evtDate), CalendarStartAtLabel=(True,), EditCalendarStartTime=(True, "6:00 PM"), EditCalendarEndDate=(True, evtDate), CalendarEndAtLabel=(True,), EditCalendarEndTime=(True, "7:00 PM"), CalendarLocation=(True, uw("Club101")), EditTransparency=(True, "FYI"), NotesBlock=(True, uw("This is a birthday party invitation")), EditTimeZone=(False, "None")) # Not visible with timezones off # Toggle allday, then make sure the right changes happened. event.SetAttr("Setting allDay", allDay=True) event.CheckDisplayedValues("Checking allday", HeadlineBlock=(True, uw("Birthday Party")), EditAllDay=(True, True), EditCalendarStartDate=(True, evtDate), CalendarStartAtLabel=(False,), EditCalendarStartTime=(False,), EditCalendarEndDate=(True, evtDate), CalendarEndAtLabel=(False,), EditCalendarEndTime=(False,), ) # Turn on timezones, turn off alldayness, and make sure the popup appears tzPrefs.showUI = True event.SetAttr("Setting explicit timezone", allDay=False, timeZone='America/Denver') event.CheckDisplayedValues("Changed Timezone", HeadlineBlock=(True, uw("Birthday Party")), EditTimeZone=(True, 'America/Denver'), EditCalendarStartDate=(True, evtDate), EditCalendarEndDate=(True, evtDate), EditCalendarStartTime=(True,), # could check the time here if I knew the local tz EditCalendarEndTime=(True,), CalendarStartAtLabel=(True,), CalendarEndAtLabel=(True,) ) # Make it recur event.SetAttr("Making it recur", recurrence="Daily", recurrenceEnd=evtRecurrenceEnd) scripting.User.idle() event.CheckDisplayedValues("Checking recurrence", EditRecurrence=(True, "Daily"), EditRecurrenceEnd=(True, evtRecurrenceEnd)) # Select the second occurrence and delete it masterEvent = EventStamp(event.item) secondEvent = QAUITestAppLib.UITestItem( masterEvent.getFirstOccurrence().getNextOccurrence(), self.logger) secondEvent.SelectItem() secondEvent.CheckDisplayedValues("Checking 2nd occurrence", EditCalendarStartDate=(True, evtSecondDate), ) secondEvent.MoveToTrash() scripting.User.idle() # Answer the recurrence question with "just this item" self.logger.startAction('Test recurrence dialog') recurrenceDialog = wx.FindWindowByName(u'RecurrenceDialog') if recurrenceDialog is None: self.logger.endAction(False, "Didn't see the recurrence dialog when deleting a recurrence instance") else: scripting.User.emulate_click(recurrenceDialog.thisButton) scripting.User.idle() self.logger.endAction(True) # Make sure the new second occurrence starts on the right date thirdEvent = QAUITestAppLib.UITestItem( masterEvent.getFirstOccurrence().getNextOccurrence(), self.logger) thirdEvent.SelectItem() thirdEvent.CheckDisplayedValues("After deleting second occurrence", HeadlineBlock=(True, uw("Birthday Party")), EditCalendarStartDate=(True, evtThirdDate), ) # Create an event in a future week futureEvent = QAUITestAppLib.UITestItem("Event", self.logger) futureEvent.SetAttr(displayName=uw("Future Weekly"), startDate=evtNextWeek, startTime="6:00 PM", recurrence="Weekly", body=uw("This is an event in the future")) futureEvent.CheckDisplayedValues("Checking future recurring event", HeadlineBlock=(True, uw("Future Weekly")), EditAllDay=(True, False), EditCalendarStartDate=(True, evtNextWeek), CalendarStartAtLabel=(True,), EditCalendarStartTime=(True, "6:00 PM"), EditCalendarEndDate=(True, evtNextWeek), CalendarEndAtLabel=(True,), EditCalendarEndTime=(True, "7:00 PM"), NotesBlock=(True, uw("This is an event in the future"))) #leave Chandler with timezones turned off tzPrefs.showUI = False
def kindToMessageObject(mailStamp): """ This method converts an item stamped as MailStamp to an email message string a Chandler C{MailMessage} object @param mailMessage: A Chandler C{MailMessage} @type mailMessage: C{MailMessage} @return: C{Message.Message} """ view = mailStamp.itsItem.itsView mailStampOccurrence, mailStampMaster = getRecurrenceMailStamps(mailStamp) isEvent = has_stamp(mailStampOccurrence, EventStamp) isTask = has_stamp(mailStampOccurrence, TaskStamp) messageObject = Message.Message() # Create a messageId if none exists mId = getattr(mailStampMaster, "messageId", None) if not mId: mId = createMessageID() populateHeader(messageObject, 'Message-ID', mId) populateEmailAddresses(mailStampMaster, messageObject) populateStaticHeaders(messageObject) if hasattr(mailStampMaster, "dateSentString"): date = mailStampMaster.dateSentString else: date = datetimeToRFC2822Date(datetime.now(view.tzinfo.default)) messageObject["Date"] = date inReplyTo = getattr(mailStampMaster, "inReplyTo", None) subject = mailStampOccurrence.subject if subject is not None: # Fixes bug 10254 where the title of a Item # that contained a new line was breaking the # the rfc2822 formatting of the outgoing message. subject = subject.replace("\n", "") if inReplyTo: messageObject["In-Reply-To"] = inReplyTo if mailStampMaster.referencesMID: messageObject["References"] = " ".join(mailStampMaster.referencesMID) populateHeader(messageObject, 'Subject', subject, encode=True) try: payload = getMessageBody(mailStampOccurrence) except AttributeError: payload = u"" if not payload: # bug 12262, Outlook doesn't like multipart/alternative if there's # no payload, so add a few carriage returns to empty bodies payload += "\r\n\r\n" if isTask or isEvent and payload and \ not payload.endswith(u"\r\n\r\n"): # Chandler outgoing Tasks and Events contain # an ics attachment. # Many mail readers add attachment icons # at the end of the message body. # This can be distracting and visually # ugly. Appending two line breaks to the # payload provides better alignment in # mail readers such as Apple Mail and # Thunderbird. payload += u"\r\n\r\n" messageObject.set_type("multipart/mixed") # Create a multipart/alernative MIME Part # that will contain the Chandler eimml and # the body of the message as alternative # parts. Doing this prevents users from seeing # the Chandler eimml which is machine readable # xml code and is not displayable to the user. alternative = MIMEMultipart("alternative") # Serialize and attach the eimml can raise ConflictsPending eimml = outbound(getPeers(mailStampMaster), mailStampMaster.itsItem, OUTBOUND_FILTERS) eimmlPayload = MIMEBase64Encode(eimml, 'text', 'eimml') # Since alternative parts are in order from least # renderable to most renderable add the eimml payload # first. alternative.attach(eimmlPayload) # Attach the body text mt = MIMEBase64Encode(payload.encode('utf-8')) # Add the email body text to the alternative part alternative.attach(mt) # Add the alternative part to the mail multipart/mixed # main content type. messageObject.attach(alternative) #XXX There is no attachement support in 1.0 #hasAttachments = mailStamp.getNumberOfAttachments() > 0 if isEvent or isTask: # Format this message as an ICalendar object from osaf.sharing import (serialize, VObjectSerializer, SharingTranslator, remindersFilter) items = [mailStampMaster.itsItem] for mod in EventStamp(mailStampMaster).modifications or []: if not checkTriageOnly(mod): items.append(mod) calendar = serialize(mailStamp.itsItem.itsView, items, SharingTranslator, VObjectSerializer, filter=remindersFilter) # don't use method REQUEST because it will cause Apple iCal to treat # the ics attachment as iMIP calendar.add('method').value = "PUBLISH" ics = calendar.serialize() # returns a UTF-8 encoded str # Attach the ICalendar object icsPayload = MIMEBase64Encode(ics, 'text', 'calendar', method='PUBLISH') # L10N: The filename of Events and Tasks emailed from Chandler fname = Header.Header(_(u"ChandlerItem.ics")).encode() icsPayload.add_header("Content-Disposition", "attachment", filename=fname) messageObject.attach(icsPayload) #XXX: There is no attachment support in 1.0 via # the MailStamp.mimeContent. Commenting out this code # for now. # #if hasAttachments: # attachments = mailStamp.getAttachments() # # for attachment in attachments: # if has_stamp(attachment, MailStamp): # # The attachment is another MailMessage # try: # rfc2822 = binaryToData(MailStamp(attachment).rfc2822Message) # except AttributeError: # rfc2822 = kindToMessageText(attachment, False) # # message = email.message_from_string(rfc2822) # rfc2822Payload = MIMEMessage(message) # messageObject.attach(rfc2822Payload) # # else: # if isinstance(attachment, MIMEText) and \ # attachment.mimeType == u"text/calendar": # icsPayload = MIMENonMultipart('text', 'calendar', \ # method='REQUEST', _charset="utf-8") # # fname = Header.Header(attachment.filename).encode() # icsPayload.add_header("Content-Disposition", "attachment", filename=fname) # icsPayload.set_payload(attachment.data.encode('utf-8')) # messageObject.attach(icsPayload) return messageObject
# and use it. (If it was an existing event, we'll reuse it.) item = items[0] # use the master, if a modification happens to be the first item item = getattr(item, 'inheritFrom', item) if item.displayName and len(item.displayName.strip()): # The displayName will contain the ics summary icsSummary = item.displayName if item.body and len(item.displayName.strip()): # The body will contain the ics description icsDesc = item.body if not has_stamp(item, MailStamp): if has_stamp(item, EventStamp): EventStamp(item).addStampToAll(MailStamp) else: ms = MailStamp(item) ms.add() mailStamp = MailStamp(item) mailStamp.fromEIMML = False return (mailStamp, icsDesc, icsSummary) return None def buildBody(bodyBuffer): if len(bodyBuffer.get('plain')): body = removeCarriageReturn(u"\n".join(bodyBuffer.get('plain')))
def onMarkAsReadEvent(self, event): selectedItems = self.__getSelectedItems(event) haveUnread = any(item for item in selectedItems if not item.read) for item in selectedItems: EventStamp(item).getMaster().itsItem.read = haveUnread
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 itemsToVObject(view, items, cal=None, filters=None): """ Iterate through items, add to cal, create a new vcalendar if needed. Consider only master events (then serialize all modifications). For now, set all timezones to Pacific. """ if filters is None: filters = () # we want filters to be iterable def makeDateTimeValue(dt, asDate=False): if asDate: return dt.date() elif dt.tzinfo == view.tzinfo.floating: return dt.replace(tzinfo=None) else: return dt def populateCommon(comp, item): """ Populate the given vevent or vtodo vobject with values for attributes common to Events or Tasks). """ if getattr(item, Note.icalUID.name, None) is None: item.icalUID = unicode(item.itsUUID) comp.add('uid').value = item.icalUID # displayName --> SUMMARY try: summary = item.displayName except AttributeError: pass else: comp.add('summary').value = summary # body --> DESCRIPTION try: description = item.body except AttributeError: pass else: if description: comp.add('description').value = description # userReminder --> VALARM if Remindable.reminders.name not in filters: firstReminder = item.getUserReminder() if firstReminder is not None: if firstReminder.absoluteTime is not None: value = firstReminder.absoluteTime else: # @@@ For now, all relative reminders are relative to starttime assert firstReminder.relativeTo == EventStamp.effectiveStartTime.name value = firstReminder.delta comp.add('valarm').add('trigger').value = value def populateCustom(comp, item): # custom properties for name, value in item.icalendarProperties.iteritems(): prop = comp.add(name) # for unrecognized properties, import stores strings, not # native types like datetimes. So value should just be a # string, not a more complicated python data structure. Don't # try to transform the value when serializing prop.isNative = False # encoding escapes characters like backslash and comma and # combines list values into a single string. This was already # done when the icalendar was imported, so don't escape again prop.encoded = True prop.value = value for name, paramstring in item.icalendarParameters.iteritems(): paramdict = comp.contents[name][0].params for paramlist in vobject.base.parseParams(paramstring): # parseParams gives a list of lists of parameters, with the # first element of each list being the name of the # parameter, followed by the parameter values, if any paramname = paramlist[0].upper() if paramname.lower() in parametersUnderstood: # parameters understood by Chandler shouldn't be stored # in icalendarParameters, but changes to which # parameters Chandler understands can lead to spurious # parameters, ignore them continue paramvalues = paramdict.setdefault(paramname, []) paramvalues.extend(paramlist[1:]) def populateEvent(comp, event): """Populate the given vobject vevent with data from event.""" populateCommon(comp, event.itsItem) try: dtstartLine = comp.add('dtstart') # allDay-ness overrides anyTime-ness if event.anyTime and not event.allDay: dtstartLine.x_osaf_anytime_param = 'TRUE' dtstartLine.value = makeDateTimeValue(event.startTime, event.anyTime or event.allDay) except AttributeError: comp.dtstart = [] # delete the dtstart that was added try: if not (event.duration == datetime.timedelta(0) or ( (event.anyTime or event.allDay) and event.duration <= oneDay)): dtendLine = comp.add('dtend') #convert Chandler's notion of allDay duration to iCalendar's if event.allDay: dtendLine.value = event.endTime.date() + oneDay else: if event.anyTime: dtendLine.x_osaf_anytime_param = 'TRUE' # anyTime should be exported as allDay for non-Chandler apps dtendLine.value = makeDateTimeValue(event.endTime, event.anyTime) except AttributeError: comp.dtend = [] # delete the dtend that was added if EventStamp.transparency.name not in filters: try: status = event.transparency.upper() # anytime events should be interpreted as not taking up time, # but all-day shouldn't if status == 'FYI' or (not event.allDay and event.anyTime): status = 'CANCELLED' comp.add('status').value = status except AttributeError: pass try: comp.add('location').value = event.location.displayName except AttributeError: pass view = event.itsItem.itsView timestamp = datetime.datetime.utcnow() comp.add('dtstamp').value = timestamp.replace(tzinfo=view.tzinfo.UTC) if event.modificationFor is not None: recurrenceid = comp.add('recurrence-id') masterEvent = event.getMaster() allDay = masterEvent.allDay or masterEvent.anyTime recurrenceid.value = makeDateTimeValue(event.recurrenceID, allDay) # logic for serializing rrules needs to move to vobject try: # hack, create RRULE line last, because it means running transformFromNative if event.getMaster() == event and event.rruleset is not None: # False because we don't want to ignore isCount for export # True because we don't want to use view.tzinfo.floating cal.vevent_list[-1].rruleset = event.createDateUtilFromRule(False, True, False) except AttributeError: logger.error('Failed to export RRULE for %s' % event.itsItem.itsUUID) # end of populateEvent function populateCustom(comp, event.itsItem) def populateModifications(event, cal): for modification in itertools.imap(EventStamp, event.modifications): for attr, val in modification.itsItem.iterModifiedAttributes(): if attr in attributesUsedWhenExporting and attr not in filters: populateEvent(cal.add('vevent'), modification) break #end helper functions def populateTask(comp, task): """Populate the given vobject vtodo with data from task.""" populateCommon(comp, task.itsItem) # @@@ [grant] Once we start writing out Event+Tasks as # VTODO, write out DUE (or maybe DTSTART) here. if Note._triageStatus.name not in filters: triageStatus = task.itsItem._triageStatus # VTODO STATUS mapping: # --------------------- # # [ICalendar] [Triage Enum] # <no value>/IN-PROCESS now (needsReply=False) # NEEDS-ACTION now (needsReply=True) # COMPLETED done # CANCELLED later if triageStatus == TriageEnum.now: if task.itsItem.needsReply: comp.add('status').value = 'needs-action' else: comp.add('status').value = 'in-process' elif triageStatus == TriageEnum.later: comp.add('status').value = 'cancelled' else: comp.add('status').value = 'completed' populateCustom(comp, task.itsItem) if cal is None: cal = vobject.iCalendar() for item in items: # main loop try: # ignore any events that aren't masters # # Note: [grant] # At the moment, we allow Event-ness to take precedence over # Task-ness. So, we serialize Event+Task objects as VEVENTs. # Part of the reason for this is that recurring VTODOs aren't # so well-supported by other iCalendar clients. For RFC 2445 # issues with VTODO+RRULE, see e.g. # <http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001134.html> if has_stamp(item, EventStamp): event = EventStamp(item) if event.getMaster() == event: populateEvent(cal.add('vevent'), event) populateModifications(event, cal) elif has_stamp(item, TaskStamp): populateTask(cal.add('vtodo'), TaskStamp(item)) except: logger.exception("Exception while exporting %s" % item) continue return cal
def startTest(self): def mondayPlus(inc=0): """return a m/d/yy date string equal to this Monday plus inc days""" today = datetime.date.today() daysUntilMonday = today.weekday() if daysUntilMonday == 6: daysUntilMonday = -1 #sunday is special case monday = today - datetime.timedelta(days=daysUntilMonday) incDay = monday + datetime.timedelta(days=inc) view = wx.GetApp().UIRepositoryView value = datetime.datetime.combine( incDay, datetime.time(0, tzinfo=view.tzinfo.default)) dateStr = shortDateFormat.format(view, value) return dateStr # resize the Chandler window to (1024,720): this test sort of crumble if the window is too small frame = wx.GetApp().mainFrame frame.SetSize((1024, 720)) # switch to calendar view testView = QAUITestAppLib.UITestView(self.logger) testView.SwitchToCalView() # make user collection, since only user # collections can be displayed as a calendar col = QAUITestAppLib.UITestItem("Collection", self.logger) evtDate = mondayPlus() evtSecondDate = mondayPlus(1) evtThirdDate = mondayPlus(2) evtRecurrenceEnd = mondayPlus(365) evtNextWeek = mondayPlus(7) # Make sure we're not showing timezones now (we'll put it back below) tzPrefs = schema.ns('osaf.pim', QAUITestAppLib.App_ns.itsView).TimezonePrefs oldTZPref = tzPrefs.showUI tzPrefs.showUI = False # Create a vanilla event; leave the timezone alone so we can make sure # it's floating. event = QAUITestAppLib.UITestItem("Event", self.logger) event.SetAttr(displayName=uw("Birthday Party"), startDate=evtDate, startTime="6:00 PM", location=uw("Club101"), status="FYI", body=uw("This is a birthday party invitation")) # Check a few things: that those attributes got set right, plus # a few defaulty things worked (timezone, endtime) event.CheckDisplayedValues( "Checking initial setup", HeadlineBlock=(True, uw("Birthday Party")), EditAllDay=(True, False), EditCalendarStartDate=(True, evtDate), CalendarStartAtLabel=(True, ), EditCalendarStartTime=(True, "6:00 PM"), EditCalendarEndDate=(True, evtDate), CalendarEndAtLabel=(True, ), EditCalendarEndTime=(True, "7:00 PM"), CalendarLocation=(True, uw("Club101")), EditTransparency=(True, "FYI"), NotesBlock=(True, uw("This is a birthday party invitation")), EditTimeZone=(False, "None")) # Not visible with timezones off # Toggle allday, then make sure the right changes happened. event.SetAttr("Setting allDay", allDay=True) event.CheckDisplayedValues( "Checking allday", HeadlineBlock=(True, uw("Birthday Party")), EditAllDay=(True, True), EditCalendarStartDate=(True, evtDate), CalendarStartAtLabel=(False, ), EditCalendarStartTime=(False, ), EditCalendarEndDate=(True, evtDate), CalendarEndAtLabel=(False, ), EditCalendarEndTime=(False, ), ) # Turn on timezones, turn off alldayness, and make sure the popup appears tzPrefs.showUI = True event.SetAttr("Setting explicit timezone", allDay=False, timeZone='America/Denver') event.CheckDisplayedValues( "Changed Timezone", HeadlineBlock=(True, uw("Birthday Party")), EditTimeZone=(True, 'America/Denver'), EditCalendarStartDate=(True, evtDate), EditCalendarEndDate=(True, evtDate), EditCalendarStartTime=( True, ), # could check the time here if I knew the local tz EditCalendarEndTime=(True, ), CalendarStartAtLabel=(True, ), CalendarEndAtLabel=(True, )) # Make it recur event.SetAttr("Making it recur", recurrence="Daily", recurrenceEnd=evtRecurrenceEnd) scripting.User.idle() event.CheckDisplayedValues("Checking recurrence", EditRecurrence=(True, "Daily"), EditRecurrenceEnd=(True, evtRecurrenceEnd)) # Select the second occurrence and delete it masterEvent = EventStamp(event.item) secondEvent = QAUITestAppLib.UITestItem( masterEvent.getFirstOccurrence().getNextOccurrence(), self.logger) secondEvent.SelectItem() secondEvent.CheckDisplayedValues( "Checking 2nd occurrence", EditCalendarStartDate=(True, evtSecondDate), ) secondEvent.MoveToTrash() scripting.User.idle() # Answer the recurrence question with "just this item" self.logger.startAction('Test recurrence dialog') recurrenceDialog = wx.FindWindowByName(u'RecurrenceDialog') if recurrenceDialog is None: self.logger.endAction( False, "Didn't see the recurrence dialog when deleting a recurrence instance" ) else: scripting.User.emulate_click(recurrenceDialog.thisButton) scripting.User.idle() self.logger.endAction(True) # Make sure the new second occurrence starts on the right date thirdEvent = QAUITestAppLib.UITestItem( masterEvent.getFirstOccurrence().getNextOccurrence(), self.logger) thirdEvent.SelectItem() thirdEvent.CheckDisplayedValues( "After deleting second occurrence", HeadlineBlock=(True, uw("Birthday Party")), EditCalendarStartDate=(True, evtThirdDate), ) # Create an event in a future week futureEvent = QAUITestAppLib.UITestItem("Event", self.logger) futureEvent.SetAttr(displayName=uw("Future Weekly"), startDate=evtNextWeek, startTime="6:00 PM", recurrence="Weekly", body=uw("This is an event in the future")) futureEvent.CheckDisplayedValues( "Checking future recurring event", HeadlineBlock=(True, uw("Future Weekly")), EditAllDay=(True, False), EditCalendarStartDate=(True, evtNextWeek), CalendarStartAtLabel=(True, ), EditCalendarStartTime=(True, "6:00 PM"), EditCalendarEndDate=(True, evtNextWeek), CalendarEndAtLabel=(True, ), EditCalendarEndTime=(True, "7:00 PM"), NotesBlock=(True, uw("This is an event in the future"))) #leave Chandler with timezones turned off tzPrefs.showUI = False
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 itemsToVObject(view, items, cal=None, filters=None): """ Iterate through items, add to cal, create a new vcalendar if needed. Consider only master events (then serialize all modifications). For now, set all timezones to Pacific. """ if filters is None: filters = () # we want filters to be iterable def makeDateTimeValue(dt, asDate=False): if asDate: return dt.date() elif dt.tzinfo == view.tzinfo.floating: return dt.replace(tzinfo=None) else: return dt def populateCommon(comp, item): """ Populate the given vevent or vtodo vobject with values for attributes common to Events or Tasks). """ if getattr(item, Note.icalUID.name, None) is None: item.icalUID = unicode(item.itsUUID) comp.add("uid").value = item.icalUID # displayName --> SUMMARY try: summary = item.displayName except AttributeError: pass else: comp.add("summary").value = summary # body --> DESCRIPTION try: description = item.body except AttributeError: pass else: if description: comp.add("description").value = description # userReminder --> VALARM if Remindable.reminders.name not in filters: firstReminder = item.getUserReminder() if firstReminder is not None: if firstReminder.absoluteTime is not None: value = firstReminder.absoluteTime else: # @@@ For now, all relative reminders are relative to starttime assert firstReminder.relativeTo == EventStamp.effectiveStartTime.name value = firstReminder.delta comp.add("valarm").add("trigger").value = value def populateCustom(comp, item): # custom properties for name, value in item.icalendarProperties.iteritems(): prop = comp.add(name) # for unrecognized properties, import stores strings, not # native types like datetimes. So value should just be a # string, not a more complicated python data structure. Don't # try to transform the value when serializing prop.isNative = False # encoding escapes characters like backslash and comma and # combines list values into a single string. This was already # done when the icalendar was imported, so don't escape again prop.encoded = True prop.value = value for name, paramstring in item.icalendarParameters.iteritems(): paramdict = comp.contents[name][0].params for paramlist in vobject.base.parseParams(paramstring): # parseParams gives a list of lists of parameters, with the # first element of each list being the name of the # parameter, followed by the parameter values, if any paramname = paramlist[0].upper() if paramname.lower() in parametersUnderstood: # parameters understood by Chandler shouldn't be stored # in icalendarParameters, but changes to which # parameters Chandler understands can lead to spurious # parameters, ignore them continue paramvalues = paramdict.setdefault(paramname, []) paramvalues.extend(paramlist[1:]) def populateEvent(comp, event): """Populate the given vobject vevent with data from event.""" populateCommon(comp, event.itsItem) try: dtstartLine = comp.add("dtstart") # allDay-ness overrides anyTime-ness if event.anyTime and not event.allDay: dtstartLine.x_osaf_anytime_param = "TRUE" dtstartLine.value = makeDateTimeValue(event.startTime, event.anyTime or event.allDay) except AttributeError: comp.dtstart = [] # delete the dtstart that was added try: if not ( event.duration == datetime.timedelta(0) or ((event.anyTime or event.allDay) and event.duration <= oneDay) ): dtendLine = comp.add("dtend") # convert Chandler's notion of allDay duration to iCalendar's if event.allDay: dtendLine.value = event.endTime.date() + oneDay else: if event.anyTime: dtendLine.x_osaf_anytime_param = "TRUE" # anyTime should be exported as allDay for non-Chandler apps dtendLine.value = makeDateTimeValue(event.endTime, event.anyTime) except AttributeError: comp.dtend = [] # delete the dtend that was added if EventStamp.transparency.name not in filters: try: status = event.transparency.upper() # anytime events should be interpreted as not taking up time, # but all-day shouldn't if status == "FYI" or (not event.allDay and event.anyTime): status = "CANCELLED" comp.add("status").value = status except AttributeError: pass try: comp.add("location").value = event.location.displayName except AttributeError: pass view = event.itsItem.itsView timestamp = datetime.datetime.utcnow() comp.add("dtstamp").value = timestamp.replace(tzinfo=view.tzinfo.UTC) if event.modificationFor is not None: recurrenceid = comp.add("recurrence-id") masterEvent = event.getMaster() allDay = masterEvent.allDay or masterEvent.anyTime recurrenceid.value = makeDateTimeValue(event.recurrenceID, allDay) # logic for serializing rrules needs to move to vobject try: # hack, create RRULE line last, because it means running transformFromNative if event.getMaster() == event and event.rruleset is not None: # False because we don't want to ignore isCount for export # True because we don't want to use view.tzinfo.floating cal.vevent_list[-1].rruleset = event.createDateUtilFromRule(False, True, False) except AttributeError: logger.error("Failed to export RRULE for %s" % event.itsItem.itsUUID) # end of populateEvent function populateCustom(comp, event.itsItem) def populateModifications(event, cal): for modification in itertools.imap(EventStamp, event.modifications): for attr, val in modification.itsItem.iterModifiedAttributes(): if attr in attributesUsedWhenExporting and attr not in filters: populateEvent(cal.add("vevent"), modification) break # end helper functions def populateTask(comp, task): """Populate the given vobject vtodo with data from task.""" populateCommon(comp, task.itsItem) # @@@ [grant] Once we start writing out Event+Tasks as # VTODO, write out DUE (or maybe DTSTART) here. if Note._triageStatus.name not in filters: triageStatus = task.itsItem._triageStatus # VTODO STATUS mapping: # --------------------- # # [ICalendar] [Triage Enum] # <no value>/IN-PROCESS now (needsReply=False) # NEEDS-ACTION now (needsReply=True) # COMPLETED done # CANCELLED later if triageStatus == TriageEnum.now: if task.itsItem.needsReply: comp.add("status").value = "needs-action" else: comp.add("status").value = "in-process" elif triageStatus == TriageEnum.later: comp.add("status").value = "cancelled" else: comp.add("status").value = "completed" populateCustom(comp, task.itsItem) if cal is None: cal = vobject.iCalendar() for item in items: # main loop try: # ignore any events that aren't masters # # Note: [grant] # At the moment, we allow Event-ness to take precedence over # Task-ness. So, we serialize Event+Task objects as VEVENTs. # Part of the reason for this is that recurring VTODOs aren't # so well-supported by other iCalendar clients. For RFC 2445 # issues with VTODO+RRULE, see e.g. # <http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001134.html> if has_stamp(item, EventStamp): event = EventStamp(item) if event.getMaster() == event: populateEvent(cal.add("vevent"), event) populateModifications(event, cal) elif has_stamp(item, TaskStamp): populateTask(cal.add("vtodo"), TaskStamp(item)) except: logger.exception("Exception while exporting %s" % item) continue return cal