- Revision
- 11436
- Author
- stearns
- Date
- 2006-08-17 11:54:38 -0700 (Thu, 17 Aug 2006)
Log Message
Second step of several reminder changes: still no user-visible changes; added internal support for absolute reminders, as well as more than one reminder per item (only one user-visible reminder is possible, though an internal reminder will be used for dealing with the startTime-reached triageStatus transition), and updated unit tests.
Modified Paths
- trunk/chandler/application/Utility.py
- trunk/chandler/parcels/osaf/pim/calendar/Calendar.py
- trunk/chandler/parcels/osaf/pim/calendar/tests/TestReminders.py
- trunk/chandler/parcels/osaf/pim/generate.py
- trunk/chandler/parcels/osaf/pim/items.py
- trunk/chandler/parcels/osaf/pim/reminders.py
- trunk/chandler/parcels/osaf/sharing/ICalendar.py
- trunk/chandler/parcels/osaf/views/detail/detail.py
- trunk/chandler/parcels/osaf/views/detail/detailblocks.py
- trunk/chandler/util/GenerateItemsFromFile.py
Diff
Modified: trunk/chandler/application/Utility.py (11435 => 11436)
--- trunk/chandler/application/Utility.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/application/Utility.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -33,7 +33,7 @@ # with your name (and some helpful text). The comment's really there just to # cause Subversion to warn you of a conflict when you update, in case someone # else changes it at the same time you do (that's why it's on the same line). -SCHEMA_VERSION = "234" # jeffrey: Add ScrollY to TimedEventCanvas +SCHEMA_VERSION = "235" # stearns: Reminder rework, part deux logger = None # initialized in initLogging()
Modified: trunk/chandler/parcels/osaf/pim/calendar/Calendar.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/pim/calendar/Calendar.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/pim/calendar/Calendar.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -35,7 +35,6 @@ from TimeZone import formatTime from osaf.pim.calendar.TimeZone import coerceTimeZone, TimeZoneInfo from osaf.pim.calendar import DateTimeUtil -from osaf.pim.reminders import Remindable, Reminder from PyICU import DateFormat, DateFormatSymbols, ICUtzinfo from datetime import datetime, time, timedelta import itertools @@ -285,8 +284,8 @@ getRecurrenceID, deleteThis, deleteThisAndFuture, deleteAll, removeRecurrence, isCustomRule, getCustomDescription, isAttributeModifiable - @group Comparison Methods for Indexing: cmpTimeAttribute, cmpStartTime, - cmpEndTime, cmpRecurEnd, cmpReminderTime + @group Comparison Methods for Indexing: cmpStartTime, + cmpEndTime, cmpRecurEnd @group Semi-Private Methods: addToCollection, changeNoModification, cleanRule, copyCollections, getEffectiveEndTime, getEffectiveStartTime, @@ -931,8 +930,13 @@ # ... and update the collections accordingly for reminder in nowExpired: - self.expiredReminders.add(reminder) self.reminders.remove(reminder) + if reminder.keepExpired: + self.expiredReminders.add(reminder) + else: + assert len(reminder.reminderItems) == 0 + assert len(reminder.expiredReminderItems) == 0 + reminder.delete() for reminder in nowNotExpired: self.reminders.add(reminder) @@ -1580,27 +1584,6 @@ # Otherwise, just do it the normal way. return super(CalendarEventMixin, self).isAttributeModifiable(attribute) - def cmpTimeAttribute(self, item, attr, useTZ=True): - """Compare item and self.attr, ignore timezones if useTZ is False.""" - itemTime = getattr(item, attr, None) - selfTime = getattr(self, attr, None) - - if itemTime is None: - if selfTime is None: - # both attributes are None, so item and self compare as equal - return 0 - else: - return -1 - elif not useTZ: - itemTime = itemTime.replace(tzinfo = None) - - if selfTime is None: - return 1 - elif not useTZ: - selfTime = selfTime.replace(tzinfo = None) - - return cmp(selfTime, itemTime) - # for use in indexing CalendarEventMixins def cmpStartTime(self, item): return self.cmpTimeAttribute(item, 'effectiveStartTime') @@ -1611,9 +1594,6 @@ def cmpRecurEnd(self, item): return self.cmpTimeAttribute(item, 'recurrenceEnd') - def cmpReminderTime(self, item): - return self.cmpTimeAttribute(item, 'reminderFireTime') - # comparisons which strip timezones def cmpStartTimeNoTZ(self, item): return self.cmpTimeAttribute(item, 'effectiveStartTime', False)
Modified: trunk/chandler/parcels/osaf/pim/calendar/tests/TestReminders.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/pim/calendar/tests/TestReminders.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/pim/calendar/tests/TestReminders.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -23,35 +23,52 @@ class ReminderTestCase(TestDomainModel.DomainModelTestCase): def testReminders(self): - # Make an event and add a reminder to it. + # Make an event and add an expired absolute reminder to it. anEvent = CalendarEvent("calendarEventItem", itsView=self.rep.view, startTime=datetime(2005,3,8,12,00, tzinfo = ICUtzinfo.default), duration=timedelta(hours=1), allDay=False, anyTime=False) - regularReminder = anEvent.makeReminder(timedelta(minutes=-10)) + + absoluteReminderTime = datetime(2005,3,8,11,00, tzinfo=ICUtzinfo.default) + absoluteReminder = anEvent.setUserReminderTime(absoluteReminderTime) + + # Make sure it got connected right: one expired absolute reminder. + self.failIf(len(anEvent.reminders)) + self.failUnless(len(anEvent.expiredReminders) == 1 \ + and anEvent.expiredReminders.first() is absoluteReminder) + self.failUnless(anEvent.userReminderTime == absoluteReminderTime) + self.failUnless(anEvent.userReminderInterval is None) + self.failUnless(anEvent.reminderFireTime == None) + + # Replace the absoluteReminder with a relative one + relativeReminderInterval = timedelta(minutes=-10) + relativeReminder = anEvent.setUserReminderInterval(relativeReminderInterval) - # Make sure it all got connected correctly - self.failUnless(len(anEvent.reminders) == 1 \ - and anEvent.reminders.first() is regularReminder) - self.failIf(len(anEvent.expiredReminders)) - self.failUnless(anEvent.reminderFireTime == datetime(2005,3,8,11,50, - tzinfo = ICUtzinfo.default)) + # Make sure it all got reconnected correctly: one expired relative reminder + self.failIf(len(anEvent.reminders)) + self.failUnless(len(anEvent.expiredReminders) == 1 \ + and anEvent.expiredReminders.first() is relativeReminder) + self.failUnless(anEvent.userReminderInterval == relativeReminderInterval) + self.failUnless(anEvent.userReminderTime is None) + self.failUnless(anEvent.reminderFireTime == None) # Snooze the reminder for 5 minutes. - snoozeReminder = anEvent.snoozeReminder(regularReminder, + snoozeReminder = anEvent.snoozeReminder(relativeReminder, timedelta(minutes=5)) - # (should move the old reminder to expired) - self.failUnless(list(anEvent.expiredReminders) == [ regularReminder ]) + # Check connections: the relative expired reminder remains, plus an + # active absolute reminder that we won't keep when it fires. + self.failUnless(list(anEvent.expiredReminders) == [ relativeReminder ]) self.failUnless(list(anEvent.reminders) == [ snoozeReminder ]) - self.failUnless(snoozeReminder.reminderItems.first() is not None) + self.failUnless(snoozeReminder.keepExpired == False) + self.failUnless(snoozeReminder.reminderItems.first() is anEvent) # Dismiss the snoozed reminder anEvent.dismissReminder(snoozeReminder) # (should destroy the snoozed reminder, leaving only the expired one) self.failIf(len(anEvent.reminders)) - self.failUnless(list(anEvent.expiredReminders) == [ regularReminder ]) - self.failUnless(regularReminder.reminderItems.first() is None) + self.failUnless(list(anEvent.expiredReminders) == [ relativeReminder ]) + self.failUnless(relativeReminder.reminderItems.first() is None) if __name__ == "__main__": unittest.main()
Modified: trunk/chandler/parcels/osaf/pim/generate.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/pim/generate.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/pim/generate.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -78,7 +78,7 @@ # Maybe a nice reminder? reminderInterval = random.choice(REMINDERS) if reminderInterval is not None: - event.makeReminder(timedelta(minutes=-reminderInterval)) + event.userReminderInterval = timedelta(minutes=-reminderInterval) # Add a location to 2/3 of the events if random.randrange(3) > 0: @@ -87,8 +87,6 @@ else: event.location = Calendar.Location.getLocation(view, random.choice(LOCATIONS)) - - event.importance = random.choice(pim.ImportanceEnum.values) event.triageStatus = random.choice(pim.TriageEnum.values) return event
Modified: trunk/chandler/parcels/osaf/pim/items.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/pim/items.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/pim/items.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -625,7 +625,32 @@ return not isSharedInAnyReadOnlyShares + # For use in indexing time-related attributes. We only use this for + # reminderFireTime here, but CalendarEventMixin uses this a lot more... + def cmpTimeAttribute(self, item, attr, useTZ=True): + """Compare item and self.attr, ignore timezones if useTZ is False.""" + itemTime = getattr(item, attr, None) + selfTime = getattr(self, attr, None) + if itemTime is None: + if selfTime is None: + # both attributes are None, so item and self compare as equal + return 0 + else: + return -1 + elif not useTZ: + itemTime = itemTime.replace(tzinfo = None) + + if selfTime is None: + return 1 + elif not useTZ: + selfTime = selfTime.replace(tzinfo = None) + + return cmp(selfTime, itemTime) + + def cmpReminderTime(self, item): + return self.cmpTimeAttribute(item, 'reminderFireTime') + """ STAMPING SUPPORT CLASSES """
Modified: trunk/chandler/parcels/osaf/pim/reminders.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/pim/reminders.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/pim/reminders.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -18,18 +18,42 @@ from datetime import datetime, time, timedelta from PyICU import ICUtzinfo +# Make a value we can use for distant (or invalid) reminder times +farFuture = datetime.max +if getattr(farFuture, 'tzInfo', None) is None: + farFuture = farFuture.replace(tzinfo=ICUtzinfo.default) class Reminder(schema.Item): - delta = schema.One( - schema.TimeDelta, - doc="The amount in advance this reminder should occur (usually negative!)", + absoluteTime = schema.One( + schema.DateTimeTZ, + defaultValue=None, + doc="If set, overrides relativeTo as the base time for this reminder" ) relativeTo = schema.One( schema.Text, - initialValue='effectiveStartTime', + defaultValue=None, + doc="The Remindable attribute that we're relative to", ) + delta = schema.One( + schema.TimeDelta, + defaultValue=timedelta(0), + doc="Offset relative to 'relativeTo' that this reminder should occur", + ) + + userCreated = schema.One( + schema.Boolean, + defaultValue=True, + doc="Is a user-created reminder?" + ) + + keepExpired = schema.One( + schema.Boolean, + defaultValue=True, + doc="Should we keep this around (in the Remindable's expiredReminders) " + "after it fires?") + reminderItems = schema.Sequence( "Remindable", inverse="reminders", @@ -42,28 +66,29 @@ initialValue=[] ) - snoozedUntil = schema.One( - schema.DateTimeTZ, - defaultValue=None - ) - schema.addClouds( - sharing = schema.Cloud(delta, relativeTo, snoozedUntil) + sharing = schema.Cloud(absoluteTime, delta, relativeTo, + userCreated, keepExpired) ) def getBaseTimeFor(self, remindable): """ - Get the relative-to time for this remindable's next reminder - so that the UI can generate a message relative to it. + Get the base time for this remindable's next reminder: + - it's our absolute time if we have one; + - otherwise, get the relative-to time from our remindable + - otherwise, use a time in the far future. """ - return getattr(remindable, self.relativeTo, datetime.max) + return self.absoluteTime or getattr(remindable, self.relativeTo) \ + or farFuture def getNextReminderTimeFor(self, remindable): """ Get the time for this remindable's next reminder """ - result = self.snoozedUntil or \ - (self.getBaseTimeFor(remindable) + self.delta) - if result.tzinfo is None: - result = result.replace(tzinfo=ICUtzinfo.default) + result = self.getBaseTimeFor(remindable) + if result != farFuture: + result += self.delta + assert result.tzinfo is not None + #if result.tzinfo is None: + #result = result.replace(tzinfo=ICUtzinfo.default) return result class Remindable(schema.Item): @@ -86,75 +111,107 @@ ) ) - def getReminderInterval(self): + def getUserReminder(self, collectionToo=False): for attr in ("reminders", "expiredReminders"): if (self.hasLocalAttributeValue(attr)): - #@@@ This assumes we've only got 0 or 1 reminders. - first = getattr(self, attr).first() - try: - return first.delta - except AttributeError: - pass + collection = getattr(self, attr) + for reminder in collection: + if reminder.userCreated: + return collectionToo and (collection, reminder) or reminder + return collectionToo and (None, None) or None - return None + def replaceUserReminder(self, **kwds): + (collection, userReminder) = self.getUserReminder(True) - def setReminderInterval(self, delta): - reminderCollection = self.reminders - firstReminder = reminderCollection.first() + if userReminder is not None: + collection.remove(userReminder) + if not (len(userReminder.reminderItems) or \ + len(userReminder.expiredReminderItems)): + userReminder.delete() - if firstReminder is None: - reminderCollection = self.expiredReminders - firstReminder = reminderCollection.first() + if kwds.get('delta') is None and kwds.get('absoluteTime') is None: + return None + + return self.makeReminder(userCreated=True, keepExpired=True, + checkExpired=True, **kwds) + + # @@@ Note: 'Calculated' APIs are provided for both relative and absolute + # user-set reminders, even though only one reminder (which can be of either + # flavor) can be set right now. The 'set' functions can replace an existing + # reminder of either flavor, but the 'get' functions ignore (that is, return + # 'None' for) reminders of the wrong flavor. + + def getUserReminderInterval(self): + userReminder = self.getUserReminder() + if userReminder is None or userReminder.absoluteTime is not None: + return None + return userReminder.delta - if firstReminder is not None: - reminderCollection.remove(firstReminder) - if not (len(firstReminder.reminderItems) or \ - len(firstReminder.expiredReminderItems)): - firstReminder.delete() + def setUserReminderInterval(self, delta): + assert hasattr(self, 'effectiveStartTime') + return self.replaceUserReminder(delta=delta, relativeTo='effectiveStartTime') - if delta is not None: - self.makeReminder(delta, checkExpired=True) - - reminderInterval = Calculated( + userReminderInterval = Calculated( schema.TimeDelta, basedOn=('reminders',), - fget=getReminderInterval, - fset=setReminderInterval, - doc="Reminder interval, computed from the first unexpired reminder." + fget=getUserReminderInterval, + fset=setUserReminderInterval, + doc="User-set reminder interval, computed from the first unexpired reminder." ) + def getUserReminderTime(self): + userReminder = self.getUserReminder() + if userReminder is None or userReminder.absoluteTime is None: + return None + return userReminder.absoluteTime + + def setUserReminderTime(self, absoluteTime): + return self.replaceUserReminder(absoluteTime=absoluteTime) + + userReminderTime = Calculated( + schema.DateTimeTZ, + basedOn=('reminders',), + fget=getUserReminderTime, + fset=setUserReminderTime, + doc="User-set absolute reminder time." + ) + def getReminderFireTime(self): """ - A simplification of the possible complexity of reminder, assumes one - or zero reminders. Returns a datetime or None. + Get the next reminder (of any kind) due to fire, or None if there aren't + any. """ - reminder = self.reminders.first() - if reminder is None: - return None - else: - return reminder.getNextReminderTimeFor(self) - + for reminder in self.reminders: + nextTime = reminder.getNextReminderTimeFor(self) + if nextTime is not None: + return nextTime + return None + + # @@@ For now, this is used to set absolute user reminders + # @@@ This is used for the next reminder of any kind (user or not), which is + # what the reminder-firing mechanism wants reminderFireTime = Calculated( schema.DateTimeTZ, basedOn=('startTime', 'allDay', 'anyTime', 'reminders'), fget=getReminderFireTime, doc="Reminder fire time, or None for no unexpired reminders") - def makeReminder(self, delta, checkExpired=False): + def makeReminder(self, checkExpired=False, **kwds): # @@@ I think the proxy code should override calls to this method # add a separate reference to this reminder to each generated event, # (or something like that). (Remindable.snoozeReminder will call this # method; that operation should only affect the actual event, not the # series) - newReminder = Reminder(None, delta=delta, itsView=self.itsView) + + newReminder = Reminder(None, itsView=self.itsView, **kwds) addThisTo = self.reminders - if checkExpired: nextTime = newReminder.getNextReminderTimeFor(self) if (nextTime is not None and nextTime < datetime.now(ICUtzinfo.default)): + assert kwds['keepExpired'], "Creating an expired reminder that isn't marked 'keepExpired'?" addThisTo = self.expiredReminders addThisTo.add(newReminder) @@ -166,8 +223,6 @@ # Make sure the next one's around, so we'll prime the reminder- # watching mechanism to alert us about it. We also check that # reminders for past events don't trigger this one. - now = datetime.now(ICUtzinfo.default) - try: getNextOccurrenceMethod = self.getNextOccurrence except AttributeError: @@ -177,15 +232,15 @@ # don't need to do anything with it; we just # want to make sure it's been instantiated # so that the next reminder will fire. - getNextOccurrenceMethod(after=now) + getNextOccurrenceMethod(after=datetime.now(ICUtzinfo.default)) # In the case of generated occurrences, the reminder # may already have fired (cf fixReminders() in # CalendarEventMixin.getNextOccurrence if reminder in self.reminders: self.reminders.remove(reminder) - if getattr(reminder, 'snoozedUntil', None) is not None: - # This is a "snooze" reminder, just toss it. + if not reminder.keepExpired: + # This is a system or "snooze" reminder, just toss it. assert len(reminder.reminderItems) == 0 assert len(reminder.expiredReminderItems) == 0 reminder.delete() @@ -194,7 +249,6 @@ self.expiredReminders.add(reminder) - def snoozeReminder(self, reminder, delay): """ Snooze this reminder for this long. """ # Dismiss the original reminder @@ -203,7 +257,9 @@ # Make a new reminder for this event newReminder = Reminder(None, itsView=self.itsView, - snoozedUntil=(datetime.now(ICUtzinfo.default) + - delay)) + absoluteTime=(datetime.now(ICUtzinfo.default) + + delay), + keepExpired=False, + userCreated=False) self.reminders.add(newReminder) return newReminder
Modified: trunk/chandler/parcels/osaf/sharing/ICalendar.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/sharing/ICalendar.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/sharing/ICalendar.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -142,9 +142,15 @@ pass if not filters or "reminders" not in filters: - firstReminder = item.reminders.first() or item.expiredReminders.first() + firstReminder = item.getUserReminder() if firstReminder is not None: - comp.add('valarm').add('trigger').value = firstReminder.delta + if firstReminder.absoluteTime is not None: + value = firstReminder.absoluteTime + else: + # @@@ For now, all relative reminders are relative to starttime + assert firstReminder.relativeTo == 'effectiveStartTime' + value = firstReminder.delta + comp.add('valarm').add('trigger').value = value if item.getAttributeValue('modificationFor', default=None) is not None: recurrenceid = comp.add('recurrence-id') @@ -434,13 +440,18 @@ # dtstart. anyTime = getattr(event.dtstart, 'x_osaf_anytime_param', None) == 'TRUE' + reminderDelta = None + reminderAbsoluteTime = None try: - reminderDelta = event.valarm.trigger.value - if type(reminderDelta) is datetime.datetime: - reminderDelta = reminderDelta - dtstart + reminderValue = event.valarm.trigger.value except AttributeError: - reminderDelta = None - + pass + else: + if type(reminderValue) is datetime.datetime: + reminderAbsolute = reminderValue + else: + assert type(reminderValue) is datetime.timedelta + reminderDelta = reminderValue if duration is None: if dtend is not None: @@ -577,11 +588,15 @@ change('location', Calendar.Location.getLocation(view, location)) - # rruleset and reminderInterval need to be set last + # rruleset and userReminderInterval/userReminderTime must be set last changeLast = [] if not filters or "reminders" not in filters: if reminderDelta is not None: - changeLast.append(('reminderInterval', reminderDelta)) + changeLast.append(('userReminderInterval', + reminderDelta)) + elif reminderAbsoluteTime is None: + changeLast.append(('userReminderTime', + reminderAbsoluteTime)) rruleset = event.rruleset if rruleset is not None:
Modified: trunk/chandler/parcels/osaf/views/detail/detail.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/views/detail/detail.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/views/detail/detail.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -746,7 +746,7 @@ """ Get the value from the specified attribute of the item. """ - return item.reminderInterval + return item.userReminderInterval def SetAttributeValue (self, item, attributeName, value): """Set the value of the attribute given by the value.
Modified: trunk/chandler/parcels/osaf/views/detail/detailblocks.py (11435 => 11436)
--- trunk/chandler/parcels/osaf/views/detail/detailblocks.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/parcels/osaf/views/detail/detailblocks.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -405,7 +405,7 @@ makeLabel(parcel, _(u'alarm'), borderTop=5), makeSpacer(parcel, width=8), makeEditor(parcel, 'EditReminder', - viewAttribute=u'reminderInterval', + viewAttribute=u'userReminderInterval', presentationStyle={ # @@@ XXX i18n: the code assumes that if the value # starts with a digit, it's a number of minutes; if not,
Modified: trunk/chandler/util/GenerateItemsFromFile.py (11435 => 11436)
--- trunk/chandler/util/GenerateItemsFromFile.py 2006-08-17 18:02:08 UTC (rev 11435) +++ trunk/chandler/util/GenerateItemsFromFile.py 2006-08-17 18:54:38 UTC (rev 11436) @@ -185,7 +185,7 @@ reminderInterval = random.choice(REMINDERS) else: reminderInterval = string.atoi(args[7]) - event.makeReminder(timedelta(minutes=-reminderInterval)) + event.userReminderInterval = timedelta(minutes=-reminderInterval) #location if args[8]=='*': # semi-random data
_______________________________________________ Commits mailing list [email protected] http://lists.osafoundation.org/mailman/listinfo/commits
