class ToolBar(Block.RectangularChild): colorStyle = schema.One(ColorStyle, defaultValue=None) toolSize = schema.One(SizeType) separatorWidth = schema.One(schema.Integer, defaultValue=5) buttons3D = schema.One(schema.Boolean, defaultValue=False) buttonsLabeled = schema.One(schema.Boolean, defaultValue=False) schema.addClouds(copying=schema.Cloud(byRef=[colorStyle])) def instantiateWidget(self): style = wx.TB_HORIZONTAL | wx.TB_MAC_NATIVE_SELECT if self.buttons3D: style |= wx.TB_3DBUTTONS else: style |= wx.TB_FLAT if self.buttonsLabeled: style |= wx.TB_TEXT return wxToolBar(self.parentBlock.widget, self.getWidgetID(), style=style) def pressed(self, itemName): toolBarItem = self.findBlockByName(itemName).widget return self.widget.GetToolState(toolBarItem.GetId()) def press(self, itemName): toolBarItem = self.findBlockByName(itemName) return self.post(toolBarItem.event, {}, toolBarItem)
class ModifyContentsEvent(BlockEvent): items = schema.Sequence(schema.Item, initialValue=[]) operation = schema.One(operationType, initialValue='add') copyItems = schema.One(schema.Boolean, initialValue=True) selectFirstItem = schema.One(schema.Boolean, initialValue=False) disambiguateItemNames = schema.One(schema.Boolean, initialValue=False) schema.addClouds(copying=schema.Cloud(byRef=[items]))
class BlockEvent(schema.Item): dispatchEnum = schema.One( dispatchEnumType, initialValue='SendToBlockByReference', ) commitAfterDispatch = schema.One(schema.Boolean, initialValue=False) destinationBlockReference = schema.One(Block) dispatchToBlockName = schema.One(schema.String) methodName = schema.One(schema.String) blockName = schema.One(schema.String) schema.addClouds(copying=schema.Cloud(byCloud=[destinationBlockReference])) def __repr__(self): # useful for debugging that i've done. i dunno if event.arguments # is guaranteed to be there? -brendano if hasattr(self, "arguments"): try: name = self.blockName except AttributeError: name = self.itsUUID return "%s, arguments=%s" % (name, repr(self.arguments)) else: return super(BlockEvent, self).__repr__()
class ContactName(items.ContentItem): "A very simple (and incomplete) representation of a person's name" firstName = schema.One(schema.String, initialValue="") lastName = schema.One(schema.String, initialValue="") contact = schema.One("Contact", inverse="contactName") schema.addClouds(sharing=schema.Cloud(firstName, lastName))
class BaseItem(Block.Block): title = schema.One(LocalizableString) helpString = schema.One(LocalizableString, defaultValue=u'') operation = schema.One(operationEnumType, defaultValue='None') location = schema.One(schema.Text, defaultValue=u'') itemLocation = schema.One(schema.Text, defaultValue=u'') event = schema.One(Block.BlockEvent, inverse=Block.BlockEvent.menuOrToolForEvent) schema.addClouds(copying=schema.Cloud(byRef=[event]))
class MyKind1(pim.ContentItem): """An example content kind""" attr1 = schema.One(schema.Text) attr2 = schema.One(schema.Text) # Typical clouds include a "copying" cloud, and a "sharing" cloud schema.addClouds(sharing=schema.Cloud( literal=[attr1, attr2], byValue=[attr3], byCloud=[attr4]))
class ContactName(items.ContentItem): "A very simple (and incomplete) representation of a person's name" firstName = schema.One(schema.Text, initialValue=u"", indexed=True) lastName = schema.One(schema.Text, initialValue=u"", indexed=True) contact = schema.One() schema.addClouds( sharing = schema.Cloud( literal = [firstName, lastName] ) )
class UserCollection(schema.Annotation): schema.kindInfo(annotates=ContentCollection) renameable = schema.One(schema.Boolean, defaultValue=True) color = schema.One(ColorType) iconName = schema.One(schema.Text, defaultValue="") colorizeIcon = schema.One(schema.Boolean, defaultValue=True) dontDisplayAsCalendar = schema.One(schema.Boolean, defaultValue=False) outOfTheBoxCollection = schema.One(schema.Boolean, defaultValue=False) canAdd = schema.One(schema.Boolean, defaultValue=True) allowOverlay = schema.One(schema.Boolean, defaultValue=True) searchMatches = schema.One(schema.Integer, defaultValue=0) checked = schema.One(schema.Boolean, defaultValue=False) """ preferredClass is used as a hint to the user-interface to choose the right view for the display, e.g. CalendarView for collections that have a preferredClass of EventStamp. """ preferredClass = schema.One(schema.Class) schema.addClouds(copying=schema.Cloud(byRef=[preferredClass]), ) def ensureColor(self): """ Make sure the collection has a color. Pick up the next color in a predefined list if none was set. """ if not hasattr(self, 'color'): self.color = schema.ns( 'osaf.usercollections', self.itsItem.itsView).collectionColors.nextColor() return self def setColor(self, colorname): """ Set the collection color by name. Raises an error if colorname doesn't exist. """ hue = None for colname, coltitle, colhue in collectionHues: if colname == colorname: hue = colhue break if hue is None: raise ValueError("Unknown color name") rgb = colorsys.hsv_to_rgb(hue / 360.0, 0.5, 1.0) self.color = ColorType(int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255), 255) def setValues(self, **kwds): for attr, value in kwds.iteritems(): setattr(self, attr, value)
class MenuItem (Block.Block, DynamicChild): menuItemKind = schema.One(menuItemKindEnumType, initialValue = 'Normal') accel = schema.One(schema.String, initialValue = '') event = schema.One(Block.BlockEvent) schema.addClouds( copying = schema.Cloud(byCloud = [event]) ) def instantiateWidget (self): # We'll need a dynamicParent's widget in order to instantiate try: if isinstance(self.dynamicParent.widget, wxMenu): return wxMenuItem(style=wxMenuItem.CalculateWXStyle(self)) except AttributeError: return None
class FlickrPhotoMixin(PhotoMixin): """ A mixin that adds flickr attributes to a Note item """ flickrID = schema.One(schema.Text) imageURL = schema.One(schema.URL) datePosted = schema.One(schema.DateTime) tags = schema.Sequence() owner = schema.One(schema.Text, indexed=True) schema.addClouds(sharing=schema.Cloud( literal=[owner, flickrID, imageURL, tags])) def _setPhoto(self, photo): if photo is not None: self.flickrID = photo.id self.displayName = photo.title self.description = photo.description.encode('utf8') self.owner = photo.owner.username if (photo.owner.realname is not None and photo.owner.realname.strip()): self.owner = photo.owner.realname self.imageURL = URL(photo.getURL(urlType="source")) self.datePosted = datetime.utcfromtimestamp(int(photo.dateposted)) self.dateTaken = dateutil.parser.parse( photo.datetaken, default=datetime.now(tz=self.itsView.tzinfo.default)) if photo.tags: self.tags = [ Tag.getTag(self.itsView, tag.text) for tag in photo.tags ] photo = property(None, _setPhoto) def __setup__(self): self.importFromURL(self.imageURL) @schema.observer(owner) def onOwnerChange(self, op, attr): self.updateDisplayWho(op, attr) def addDisplayWhos(self, whos): super(FlickrPhotoMixin, self).addDisplayWhos(whos) owner = getattr(self, 'owner', None) if owner is not None: whos.append((16, owner, 'owner'))
class DynamicContainer(RefCollectionDictionary, DynamicBlock): """ A block whose children are built dynamically, when the Active View changes. This list of children is in "dynamicChildren" and the back pointer is in "dynamicParent". """ dynamicChildren = schema.Sequence( Block.Block, otherName = 'dynamicParent', ) collectionSpecifier = schema.One(redirectTo = 'dynamicChildren') schema.addClouds( copying = schema.Cloud(byCloud = [dynamicChildren]) ) def itemNameAccessor(self, item): """ Use blockName for the accessor """ return item.blockName def isDynamicContainer (self): return True def populateFromStaticChildren (self): # copy our static children as a useful starting point self.dynamicChildren.clear () for block in self.childrenBlocks: self[block.blockName] = block def ensureDynamicChildren (self): """ Make sure we have a DynamicChildren hierarchy, since all my subclasses use that hierarchy when they synchronize. If there is no DynamicChildren built, then initialize it from the childrenBlocks hierarchy. """ try: children = len (self.dynamicChildren) except AttributeError: children = 0 if not children: self.populateFromStaticChildren ()
class ColorStyle(Style): """ Class for Color Style Attributes for backgroundColor and foregroundColor """ foregroundColor = schema.One( ColorType, initialValue = ColorType(0, 0, 0, 255), ) backgroundColor = schema.One( ColorType, initialValue = ColorType(255, 255, 255, 255), ) schema.addClouds( sharing = schema.Cloud(foregroundColor, backgroundColor) )
class Toolbar(Block.RectangularChild, DynamicContainer): colorStyle = schema.One('osaf.framework.blocks.Styles.ColorStyle') toolSize = schema.One('osaf.framework.blocks.DocumentTypes.SizeType') separatorWidth = schema.One(schema.Integer, initialValue = 5) buttons3D = schema.One(schema.Boolean, initialValue = False) buttonsLabeled = schema.One(schema.Boolean, initialValue = False) schema.addClouds( copying = schema.Cloud(byRef=[colorStyle]) ) def instantiateWidget (self): self.ensureDynamicChildren () # @@@DLD - remove this workaround for previous wxWidgets issues heightGutter = 9 if self.buttonsLabeled: heightGutter += 14 toolbar = wxToolbar(self.parentBlock.widget, Block.Block.getWidgetID(self), wx.DefaultPosition, (-1, self.toolSize.height+heightGutter), style=self.calculate_wxStyle()) # set the tool bitmap size right away toolbar.SetToolBitmapSize((self.toolSize.width, self.toolSize.height)) """ # @@@davids - debugging code - might eventually be useful to have UI items name themselves if (self.toolSize.width >= 26): toolbar.SetLabel("Main") else: toolbar.SetLabel("Markup") """ return toolbar def calculate_wxStyle (self): style = wx.TB_HORIZONTAL if self.buttons3D: style |= wx.TB_3DBUTTONS else: style |= wx.TB_FLAT if self.buttonsLabeled: style |= wx.TB_TEXT return style
class TaskStamp(Stamp): """ TaskStamp is the bag of Task-specific attributes. """ schema.kindInfo(annotates = notes.Note) __use_collection__ = True requestor = schema.One( Contact, description = "Issues:\n" ' Type could be Contact, EmailAddress or String\n' ' Think about using the icalendar terminology\n', inverse = Contact.requestedTasks, ) requestee = schema.Sequence( Contact, description = "Issues:\n" ' Type could be Contact, EmailAddress or String\n' ' Think about using the icalendar terminology\n', inverse = Contact.taskRequests, ) # Redirections @apply def summary(): def fget(self): return self.itsItem.displayName def fset(self, value): self.itsItem.displayName = value return schema.Calculated(schema.Text, (items.ContentItem.displayName,), fget, fset) schema.addClouds( copying = schema.Cloud( requestor, requestee ) ) def InitOutgoingAttributes (self): self.itsItem.InitOutgoingAttributes()
class PreviewArea(CalendarRangeBlock): timeCharacterStyle = schema.One(Styles.CharacterStyle) eventCharacterStyle = schema.One(Styles.CharacterStyle) linkCharacterStyle = schema.One(Styles.CharacterStyle) miniCalendar = schema.One(inverse=MiniCalendar.previewArea, defaultValue=None) schema.addClouds(copying=schema.Cloud( byRef=[timeCharacterStyle, eventCharacterStyle, linkCharacterStyle], byCloud=[miniCalendar])) schema.initialValues(rangeIncrement=lambda self: one_day) def onSetContentsEvent(self, event): #We want to ignore, because view changes could come in here, and we #never want to change our collection pass def onSelectAllEventUpdateUI(self, event): event.arguments['Enable'] = False def instantiateWidget(self): if not self.getHasBeenRendered(): self.setRange(datetime.now().date()) self.setHasBeenRendered() if wx.Platform == '__WXMAC__': # on the Mac, borders around the minical and preview area look weird, # but we want one around our parent. Modifying our parent is quite # a hack, but it works rather nicely. self.parentBlock.widget.SetWindowStyle(wx.BORDER_SIMPLE) return wxPreviewArea(self.parentBlock.widget, self.getWidgetID(), timeCharStyle=self.timeCharacterStyle, eventCharStyle=self.eventCharacterStyle, linkCharStyle=self.linkCharacterStyle) def onItemNotification(self, notificationType, data): # Delegate notifications to the block self.widget.onItemNotification(notificationType, data) def activeViewChanged(self): self.synchronizeWidget()
class MyKind1(ContentItem): """An example content kind""" attr1 = schema.One(schema.String, displayName="Attribute 1") schema.kindInfo( displayName = "Example Kind" ) # redirection attributes who = schema.Role(redirectTo="attr1") attr2 = schema.One(schema.String, displayName="Attribute 2") # Typical clouds include a "copying" cloud, and a "sharing" cloud schema.addClouds( sharing = schema.Cloud(attr1, attr2) )
class IntersectionCollection(AbstractCollection): """ """ schema.kindInfo( displayName="IntersectionCollection" ) left = schema.One(AbstractCollection, initialValue=None) right = schema.One(AbstractCollection, initialValue=None) schema.addClouds( copying = schema.Cloud(byCloud=[left, right]), ) def onValueChanged(self, name): if name == "left" or name == "right": try: if self.left != None and self.right != None: self.rep = Intersection((self.left, "rep"),(self.right, "rep")) except AttributeError: pass
class FilteredCollection(AbstractCollection): """ """ schema.kindInfo( displayName="FilteredCollection" ) source = schema.One(AbstractCollection, initialValue=None) filterExpression = schema.One(schema.String, initialValue="") schema.addClouds( copying = schema.Cloud(byCloud=[source]), ) def onValueChanged(self, name): if name == "source" or name == "filterExpression": try: if self.source != None and self.filterExpression != "": s = "lambda item: %s" % self.filterExpression self.rep = FilteredSet((self.source, "rep"), eval(s)) except AttributeError, ae: print ae pass
class RecurrenceRule(items.ContentItem): """One rule defining recurrence for an item.""" freq = schema.One(FrequencyEnum, defaultValue="weekly") isCount = schema.One( schema.Boolean, doc="If True, calculate and export count instead of until", defaultValue=False) until = schema.One(schema.DateTimeTZ, ) untilIsDate = schema.One( schema.Boolean, doc="If True, treat until as an inclusive date, use until + 23:59 " "for until", defaultValue=True) interval = schema.One(schema.Integer, defaultValue=1) wkst = schema.One(WeekdayEnum, defaultValue=None) bysetpos = schema.Sequence(schema.Integer, defaultValue=None) bymonth = schema.Sequence(schema.Integer, defaultValue=None) bymonthday = schema.Sequence(schema.Integer, defaultValue=None) byyearday = schema.Sequence(schema.Integer, defaultValue=None) byweekno = schema.Sequence(schema.Integer, defaultValue=None) byweekday = schema.Sequence(WeekdayAndPositionStruct, defaultValue=None) byhour = schema.Sequence(schema.Integer, defaultValue=None) byminute = schema.Sequence(schema.Integer, defaultValue=None) bysecond = schema.Sequence(schema.Integer, defaultValue=None) rruleFor = schema.One() # inverse of RecurrenceRuleSet.rruleset exruleFor = schema.One() # inverse of RecurrenceRuleSet.exrules schema.addClouds(sharing=schema.Cloud(literal=[ freq, isCount, until, untilIsDate, interval, wkst, bysetpos, bymonth, bymonthday, byyearday, byweekno, byweekday, byhour, byminute, bysecond ])) normalNames = "interval", "until" listNames = ("bysetpos", "bymonth", "bymonthday", "byyearday", "byweekno", "byhour", "byminute", "bysecond") specialNames = "wkst", "byweekday", "freq" notSpecialNames = ("interval", "until", "bysetpos", "bymonth", "bymonthday", "byyearday", "byweekno", "byhour", "byminute", "bysecond") @schema.observer(interval, until, bysetpos, bymonth, bymonthday, byyearday, byweekno, byhour, byminute, bysecond, wkst, byweekday, freq) def onRecurrenceChanged(self, op, name): """If the rule changes, update any associated events.""" for ruletype in ('rruleFor', 'exruleFor'): if self.hasLocalAttributeValue(ruletype): getattr(self, ruletype).onRuleSetChanged(op, 'rrules') # dateutil automatically sets these from dtstart, we don't want these # unless their length is greater than 1. interpretedNames = "byhour", "byminute", "bysecond" WEEKDAYS = ('monday', 'tuesday', 'wednesday', 'thursday', 'friday') def isWeekdayRule(self): if (self.freq == 'weekly' and self.interval == 1 and len(self.byweekday or []) == len(self.WEEKDAYS) and set(self.WEEKDAYS) == set( x.weekday for x in self.byweekday if x.selector == 0)): return True def calculatedUntil(self): """ Return until or until + 23:59, depending on untilIsDate. Will return None if there's no 'until' (so don't assume you can compare this value with a datetime directly!) @rtype: C{datetime} or C{None} """ try: until = self.until except AttributeError: return None if self.untilIsDate: return until.replace(hour=23, minute=59) else: return until def createDateUtilFromRule(self, dtstart, ignoreIsCount=True, convertFloating=False): """Return an appropriate dateutil.rrule.rrule. @param dtstart: The start time for the recurrence rule @type dtstart: C{datetime} @param ignoreIsCount: Whether the isCount flag should be used to convert until endtimes to a count. Converting to count takes extra cycles and is only necessary when the rule is going to be serialized @type ignoreIsCount: C{bool} @param convertFloating: Whether or not to allow view.tzinfo.floating in datetimes of the rruleset. If C{True}, naive datetimes are used instead. This is needed for exporting floating events to icalendar format. @type convertFloating: C{bool} @rtype: C{dateutil.rrule.rrule} """ tzinfo = dtstart.tzinfo view = self.itsView def coerceIfDatetime(value): if isinstance(value, datetime): if convertFloating and tzinfo == view.tzinfo.floating: value = coerceTimeZone(view, value, tzinfo).replace(tzinfo=None) else: value = coerceTimeZone(view, value, tzinfo) return value # TODO: more comments kwargs = dict( (k, getattr(self, k, None)) for k in self.notSpecialNames) for key in self.specialNames: value = coerceIfDatetime(getattr(self, key)) if value is not None: kwargs[key] = toDateUtil(value) if hasattr(self, 'until'): kwargs['until'] = coerceIfDatetime(self.calculatedUntil()) rule = rrule(dtstart=coerceIfDatetime(dtstart), **kwargs) if ignoreIsCount or not self.isCount or not hasattr(self, 'until'): return rule else: # modifying in place may screw up cache, fix when we turn # on caching rule._count = rule.count() rule._until = None return rule def setRuleFromDateUtil(self, rrule): """Extract attributes from rrule, set them in self. @param rrule: The rule to marshall into Chandler @type rrule: C{dateutil.rrule.rrule} """ view = self.itsView self.untilIsDate = False until = None # assume no limit if rrule._count is not None: self.isCount = True until = rrule[-1] self.wkst = fromDateUtilWeekday(rrule._wkst) self.freq = fromDateUtilFrequency(rrule._freq) # ignore byweekday if freq is WEEKLY and day correlates with dtstart # because it was automatically set by dateutil if rrule._freq != dateutil.rrule.WEEKLY or \ len(rrule._byweekday or ()) != 1 or \ rrule._dtstart.weekday() != rrule._byweekday[0]: listOfDayTuples = [] if rrule._byweekday: # Day tuples are (dayOrdinal, n-th week of the month), # 0 means all weeks listOfDayTuples = [(day, 0) for day in rrule._byweekday] if rrule._bynweekday is not None: listOfDayTuples.extend(rrule._bynweekday) if len(listOfDayTuples) > 0: self.byweekday = [] for day, n in listOfDayTuples: day = fromDateUtilWeekday(day) self.byweekday.append(WeekdayAndPositionStruct(day, n)) if rrule._until is not None: until = rrule._until if rrule._interval != 1: self.interval = rrule._interval if until is None: if self.hasLocalAttributeValue('until'): del self.until else: if until.tzinfo is None: self.until = until.replace(tzinfo=view.tzinfo.floating) else: self.until = coerceTimeZone(view, until, view.tzinfo.default) for key in self.listNames: # TODO: cache getattr(rrule, '_' + key) value = getattr(rrule, '_' + key) if key == 'bymonthday': if value is not None: value += (rrule._bynmonthday or ()) if value is not None and \ (key not in self.interpretedNames or \ len(value) > 1): # cast tuples to list setattr(self, key, list(value)) # bymonthday and bymonth may be set automatically by dateutil, if so, # unset them if rrule._freq in (dateutil.rrule.MONTHLY, dateutil.rrule.YEARLY): if len(rrule._bymonthday) == 1 and len(rrule._bynmonthday) == 0: if rrule._bymonthday[0] == rrule._dtstart.day: del self.bymonthday if rrule._freq == dateutil.rrule.YEARLY: if len(rrule._bymonth or ()) == 1 and \ rrule._byweekday is None and \ len(rrule._bynweekday or ()) == 0: if rrule._bymonth[0] == rrule._dtstart.month: del self.bymonth def getPreviousRecurrenceID(self, dtstart, recurrenceID): """Return the date of the previous recurrenceID, or None. @param dtstart: The start time for the recurrence rule @type dtstart: C{datetime} @param recurrenceID: The current recurrenceID @type recurrenceID: C{datetime} @rtype: C{datetime} or C{None} """ return self.createDateUtilFromRule(dtstart).before(recurrenceID) def moveUntilBefore(self, dtstart, recurrenceID): """Find the previous recurrenceID, set UNTIL to match it. @param dtstart: The start time for the recurrence rule @type dtstart: C{datetime} @param recurrenceID: The current recurrenceID @type recurrenceID: C{datetime} """ previous = self.getPreviousRecurrenceID(dtstart, recurrenceID) assert previous is not None self.until = previous self.untilIsDate = False
class Reminder(schema.Item): """ The base class for reminders. Note that this only supports 'custom' (fixed-time) reminders; the more familiar relative reminders are defined in Calendar.py on as RelativeReminder. This resolves some unfortunate circular import dependency issues we had in the past. """ # Make values we can use for distant (or invalid) reminder times farFuture = datetime.max if farFuture.tzinfo is None: farFuture = farFuture.replace(tzinfo=ICUtzinfo.getInstance('UTC')) distantPast = datetime.min if distantPast.tzinfo is None: distantPast = distantPast.replace(tzinfo=ICUtzinfo.getInstance('UTC')) absoluteTime = schema.One( schema.DateTimeTZ, defaultValue=None, doc="If set, overrides relativeTo as the base time for this reminder" ) # These flags represent what kind of reminder this is: # userCreated promptUser # True True a relative or absolute user reminder, usually # created in the detail view (or possibly, imported # as ICalendar) # False False An internal reminder relative to the # the effectiveStartTime of an event, used # to update its triageStatus when it fires. # # @@@ [grant] For now, userCreated and promptUser are identical... It's # not so clear to me whether we need both. # userCreated = schema.One( schema.Boolean, defaultValue=True, doc="Is a user-created reminder?" ) promptUser = schema.One( schema.Boolean, defaultValue=True, doc="Should we show this reminder to the user when it goes off?") reminderItem = schema.One( defaultValue=None, ) pendingEntries = schema.Sequence( PendingReminderEntry, defaultValue=Empty, inverse=PendingReminderEntry.reminder, doc="What user-created reminders have fired, and not been " \ "dismissed or snoozed?" ) nextPoll = schema.One( schema.DateTimeTZ, doc="When next will something interesting happen with this reminder?" \ "Set to reminder.farFuture if this reminder has expired.", defaultValue=None, ) description = schema.One( schema.Text, doc="End-user text description of this reminder. Currently unused by " "Chandler.", ) duration = schema.One( schema.TimeDelta, doc="Reminder DURATION (a la ICalendar VALARM); unused by Chandler.", defaultValue=timedelta(0), ) repeat = schema.One( schema.Integer, doc="Reminder REPEAT (a la ICalendar VALARM); unused by Chandler.", defaultValue=0, ) schema.addClouds( sharing = schema.Cloud( literal = [absoluteTime, userCreated, promptUser] ), copying = schema.Cloud( literal = [absoluteTime, userCreated, promptUser, nextPoll] ) ) def onItemDelete(self, view, deferring): if self.pendingEntries: pending = list(self.pendingEntries) self.pendingEntries.clear() for entry in pending: entry.delete(recursive=True) def updatePending(self, when=None): """ The method makes sure that the Reminder's list of PendingReminderEntries is up-to-date. This involves adding new entries to the list, via the Remindable.reminderFired() method. Also, the C{nextPoll} attribute is updated to the next time something interesting will happen (i.e. another item should be reminded); this may be C{Reminder.farFuture} if the C{Reminder} has no more items to process. @param when: The time to update to. You can pass C{None}, in which case C{datetime.now()} is used. @type when: C{datetime}. """ if when is None: when = datetime.now(self.itsView.tzinfo.default) if self.nextPoll is None: # No value for nextPoll means we've just been initialized. # If we're in the past, we treat ourselves as expired. if self.absoluteTime >= when: self.nextPoll = self.absoluteTime else: self.nextPoll = self.farFuture if self.nextPoll != self.farFuture and when >= self.absoluteTime: self.reminderItem.reminderFired(self, self.absoluteTime) self.nextPoll = self.farFuture self._checkExpired() def _checkExpired(self): if self.isExpired() and not self.userCreated: self.delete(True) def dismissItem(self, item): pendingEntries = self.pendingEntries toDismiss = list(p for p in pendingEntries if p.item is item) assert len(toDismiss), "Attempt to dismiss item non-pending item %r" % ( item) for item in toDismiss: item.delete(recursive=True) if not self.pendingEntries: self.nextPoll = self.farFuture self._checkExpired() def snoozeItem(self, item, delta): nextPoll = self.nextPoll pendingEntries = self.pendingEntries toSnooze = list(p for p in pendingEntries if p.item is item) assert len(toSnooze), "Attempt to snooze item non-pending item %r" % ( item) when = datetime.now(self.itsView.tzinfo.default) + delta for item in toSnooze: item.when = when item.snoozed = True nextPoll = min(nextPoll, item.when) self.nextPoll = nextPoll def getItemBaseTime(self, item): """ Return the time we would fire for this item (independent of if it has been snoozed?) """ return self.absoluteTime or Reminder.farFuture getReminderTime = getItemBaseTime def isExpired(self): return (not self.pendingEntries and self.nextPoll is not None and self.nextPoll >= Reminder.farFuture) @schema.observer(reminderItem) def reminderItemChanged(self, op, attr): if op == 'remove': if self.pendingEntries: # @@@ [grant] Check this! toRemove = list(p for p in self.pendingEntries if isDead(p.item)) for pending in toRemove: self.pendingEntries.remove(pending) if not self.pendingEntries: self.delete(True) @classmethod def defaultTime(cls, view): """ Something's creating a reminder and needs a default time. We'll return 5PM today if that's in the future; otherwise, 8AM tomorrow. """ # start with today at 5PM t = datetime.now(tz=view.tzinfo.default)\ .replace(hour=17, minute=0, second=0, microsecond=0) now = datetime.now(tz=view.tzinfo.default) if t < now: # Make it tomorrow morning at 8AM (eg, 15 hours later) t += timedelta(hours=15) return t @classmethod def getPendingTuples(cls, view, when): """ Return a list of all reminder tuples with fire times in the past, sorted by reminder time. Each tuple contains (reminderTime, remindable, reminder). """ allFutureReminders = schema.ns('osaf.pim', view).allFutureReminders remindersToPoll = [] for reminder in allFutureReminders.iterindexvalues('reminderPoll'): if reminder.nextPoll is None or reminder.nextPoll <= when: remindersToPoll.append(reminder) else: break for reminder in remindersToPoll: reminder.updatePending(when) pendingKind = PendingReminderEntry.getKind(view) trash = schema.ns("osaf.pim", view).trashCollection resultTuples = [] for entry in pendingKind.iterItems(): thisTuple = tuple(getattr(entry, attr, None) for attr in ('when', 'item', 'reminder')) # Show everything except reminders in the trash, and # reminders that have been snoozed into the future. Also, # don't return any "dead" items in the tuple, and check # for missing attributes (bug 11415). if (thisTuple[0] is not None and not isDead(thisTuple[1]) and not isDead(thisTuple[2]) and not thisTuple[1] in trash and not (entry.snoozed and thisTuple[0] > when)): resultTuples.append(thisTuple) resultTuples.sort(key=lambda t: t[0]) return resultTuples
class Remindable(schema.Item): reminders = schema.Sequence( Reminder, inverse=Reminder.reminderItem, defaultValue=Empty, ) schema.addClouds( copying = schema.Cloud(reminders), sharing = schema.Cloud( byCloud = [reminders] ) ) def InitOutgoingAttributes(self): pass def onItemDelete(self, view, deferring): for rem in self.reminders: rem.delete(recursive=True) def getUserReminder(self, expiredToo=True): """ Get the user reminder on this item. There's supposed to be only one; it could be relative or absolute. We'll look in the 'reminders' reflist, and allow expired reminders only if expiredToo is set to False. """ # @@@ Note: This code is reimplemented in the index for the dashboard # calendar column: be sure to change that if you change this! for reminder in self.reminders: if reminder.userCreated: if expiredToo or not reminder.isExpired(): return reminder # @@@ Note: 'Calculated' APIs are provided for only absolute user-set # reminders. Relative reminders, which currently can only apply to # EventStamp instances, can be set via similar APIs on EventStamp. # Currently 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 getUserReminderTime(self): userReminder = self.getUserReminder() if userReminder is None or userReminder.absoluteTime is None: return None return userReminder.absoluteTime def setUserReminderTime(self, absoluteTime): existing = self.getUserReminder() if absoluteTime is not None: retval = Reminder(itsView=self.itsView, absoluteTime=absoluteTime, reminderItem=self) else: retval = None if existing is not None: existing.delete(recursive=True) return retval userReminderTime = schema.Calculated( schema.DateTimeTZ, basedOn=(reminders,), fget=getUserReminderTime, fset=setUserReminderTime, doc="User-set absolute reminder time." ) @schema.observer(reminders) def onRemindersChanged(self, op, attr): logger.debug("Hey, onRemindersChanged called!") self.updateDisplayDate(op, attr) def addDisplayDates(self, dates, now): """ Subclasses will override this to add relevant dates to this list; each should be a tuple, (priority, dateTimeValue, 'attributeName'). """ # Add our reminder, if we have one reminder = self.getUserReminder() if reminder is not None: reminderTime = reminder.getReminderTime(self) # displayDate should be base time for RelativeReminder, bug 12246 # for absolute reminders, getItemBaseTime matches reminderTime displayDate = reminder.getItemBaseTime(self) if reminderTime not in (None, Reminder.farFuture): dates.append((30 if reminderTime < now else 10, displayDate, 'reminder')) def reminderFired(self, reminder, when): """ Called when a reminder's fire date (whether snoozed or not) rolls around. This is overridden by subclasses; e.g. ContentItem uses it to set triage status, whereas Occurrence makes sure that the triage status change is a THIS change. The Remindable implementation makes sure that a PendingReminderEntry is created, if necessary, if reminder is userCreated. """ for pending in reminder.pendingEntries: if pending.item is self: break else: if reminder.userCreated: # No matching item, so add one pending = PendingReminderEntry(itsView=self.itsView, item=self, reminder=reminder, when=when) else: pending = None return pending
class PhotoMixin(pim.ContentItem): dateTaken = schema.One(schema.DateTimeTZ) file = schema.One(schema.Text) exif = schema.Mapping(schema.Text, initialValue={}) photoBody = schema.One(schema.Lob) @schema.observer(photoBody) def onPhotoBodyChanged(self, op, attribute): self.processEXIF() schema.addClouds(sharing=schema.Cloud(literal=[dateTaken, photoBody])) def importFromFile(self, path): if isinstance(path, unicode): path = path.encode('utf8') data = file(path, "rb").read() (mimetype, encoding) = mimetypes.guess_type(path) self.photoBody = self.itsView.createLob(data, mimetype=mimetype, compression='bz2') def importFromURL(self, url): if isinstance(url, URL): url = str(url) data = urllib2.urlopen(url).read() (mimetype, encoding) = mimetypes.guess_type(url) self.photoBody = self.itsView.createLob(data, mimetype=mimetype, compression='bz2') def exportToFile(self, path): if isinstance(path, unicode): path = path.encode('utf8') input = self.photoBody.getInputStream() data = input.read() input.close() out = file(path, "wb") out.write(data) out.close() def processEXIF(self): if hasattr(self, 'photoBody'): input = self.photoBody.getInputStream() else: input = file(self.file, 'r') data = input.read() input.close() stream = cStringIO.StringIO(data) try: exif = EXIF.process_file(stream) # First try DateTimeOriginal, falling back to DateTime takenString = str( exif.get('EXIF DateTimeOriginal', exif['Image DateTime'])) timestamp = time.mktime( time.strptime(takenString, "%Y:%m:%d %H:%M:%S")) self.dateTaken = datetime.fromtimestamp(timestamp) if self.dateTaken.tzinfo is None: self.dateTaken = self.dateTaken.replace( tzinfo=self.itsView.tzinfo.default) self.exif = {} for (key, value) in exif.iteritems(): if isinstance(value, EXIF.IFD_Tag): self.exif[key] = unicode(value.printable) else: self.exif[key] = unicode(value) except Exception, e: logger.debug("Couldn't process EXIF of Photo %s (%s)" % \ (self.itsPath, e))
class ContentItem(Triageable): """ Content Item Content Item is the abstract super-kind for things like Contacts, Calendar Events, Tasks, Mail Messages, and Notes. Content Items are user-level items, which a user might file, categorize, share, and delete. Examples: - a Calendar Event -- 'Lunch with Tug' - a Contact -- 'Terry Smith' - a Task -- 'mail 1040 to IRS' """ isProxy = False displayName = schema.One(LocalizableString, defaultValue=u"", indexed=True) body = schema.One( LocalizableString, indexed=True, defaultValue=u"", doc="All Content Items may have a body to contain notes. It's " "not decided yet whether this body would instead contain the " "payload for resource items such as presentations or " "spreadsheets -- resource items haven't been nailed down " "yet -- but the payload may be different from the notes because " "payload needs to know MIME type, etc.") creator = schema.One( # Contact doc="Link to the contact who created the item.") modifiedFlags = schema.Many( Modification, defaultValue=Empty, description='Used to track the modification state of the item') lastModified = schema.One( schema.DateTimeTZ, doc="DateTime (including timezone) this item was last modified", defaultValue=None, ) lastModifiedBy = schema.One( doc="Link to the EmailAddress who last modified the item.", defaultValue=None) lastModification = schema.One( Modification, doc="What the last modification was.", defaultValue=Modification.created, ) BYLINE_FORMATS = { Modification.created: ( _(u"Created by %(user)s on %(date)s %(tz)s"), _(u"Created on %(date)s %(tz)s"), ), Modification.edited: ( _(u"Edited by %(user)s on %(date)s %(tz)s"), _(u"Edited on %(date)s %(tz)s"), ), Modification.updated: ( _(u"Updated by %(user)s on %(date)s %(tz)s"), _(u"Updated on %(date)s %(tz)s"), ), Modification.sent: ( _(u"Sent by %(user)s on %(date)s %(tz)s"), _(u"Sent on %(date)s %(tz)s"), ), Modification.queued: ( _(u"Queued by %(user)s on %(date)s %(tz)s"), _(u"Queued on %(date)s %(tz)s"), ), } def getByline(self): lastModification = self.lastModification assert lastModification in self.BYLINE_FORMATS fmt, noUserFmt = self.BYLINE_FORMATS[lastModification] # fall back to createdOn view = self.itsView lastModified = (self.lastModified or getattr(self, 'createdOn', None) or datetime.now(view.tzinfo.default)) shortDateTimeFormat = schema.importString( "osaf.pim.shortDateTimeFormat") date = shortDateTimeFormat.format(view, lastModified) tzPrefs = schema.ns('osaf.pim', view).TimezonePrefs if tzPrefs.showUI: from calendar.TimeZone import shortTZ tzName = shortTZ(view, lastModified) else: tzName = u'' user = self.lastModifiedBy if user: result = fmt % dict(user=user.getLabel(), date=date, tz=tzName) else: result = noUserFmt % dict(date=date, tz=tzName) return result.strip() error = schema.One( schema.Text, doc="A user-visible string containing the last error that occurred. " "Typically, this should be set by the sharing or email layers when " "a conflict or delivery problem occurs.", defaultValue=None) byline = schema.Calculated(schema.Text, basedOn=(modifiedFlags, lastModified, lastModification, lastModifiedBy), fget=getByline) importance = schema.One( ImportanceEnum, doc="Most items are of normal importance (no value need be shown), " "however some things may be flagged either highly important or " "merely 'fyi'. This attribute is also used in the mail schema, so " "we shouldn't make any changes here that would break e-mail " "interoperability features.", defaultValue="normal", ) mine = schema.One(schema.Boolean, defaultValue=True) private = schema.One(schema.Boolean, defaultValue=False) read = schema.One(schema.Boolean, defaultValue=False, doc="A flag indicating whether the this item has " "been 'viewed' by the user") needsReply = schema.One( schema.Boolean, defaultValue=False, doc="A flag indicating that the user wants to reply to this item") createdOn = schema.One(schema.DateTimeTZ, doc="DateTime this item was created") # ContentItem instances can be put into ListCollections and AppCollections collections = schema.Sequence( notify=True) # inverse=collections.inclusions # ContentItem instances can be excluded by AppCollections excludedBy = schema.Sequence() # ContentItem instances can be put into SmartCollections (which define # the other end of this biref) appearsIn = schema.Sequence() # The date used for sorting the Date column displayDate = schema.One(schema.DateTimeTZ, indexed=True) displayDateSource = schema.One(schema.Importable) # The value displayed (and sorted) for the Who column. displayWho = schema.One(schema.Text, indexed=True) displayWhoSource = schema.One(schema.Importable) schema.addClouds(sharing=schema.Cloud( literal=["displayName", body, createdOn, "description"], byValue=[lastModifiedBy]), copying=schema.Cloud()) schema.initialValues( createdOn=lambda self: datetime.now(self.itsView.tzinfo.default)) def __str__(self): if self.isStale(): return super(ContentItem, self).__str__() # Stale items can't access their attributes return self.__unicode__().encode('utf8') def __unicode__(self): if self.isStale(): return super(ContentItem, self).__unicode__() return unicode( getattr(self, 'displayName', self.itsName) or self.itsUUID.str64()) def InitOutgoingAttributes(self): """ Init any attributes on ourself that are appropriate for a new outgoing item. """ super(ContentItem, self).InitOutgoingAttributes() # default the displayName self.displayName = messages.UNTITLED if not self.hasLocalAttributeValue('lastModifiedBy'): self.lastModifiedBy = self.getMyModifiedByAddress() def ExportItemData(self, clipboardHandler): # Create data for this kind of item in the clipboard handler # The data is used for Drag and Drop or Cut and Paste try: super(ContentItem, self).ExportItemData(clipboardHandler) except AttributeError: pass # Let the clipboard handler know we've got a ContentItem to export clipboardHandler.ExportItemFormat(self, 'ContentItem') def onItemDelete(self, view, deferring): # Hook for stamp deletion ... from stamping import Stamp for stampObject in Stamp(self).stamps: onItemDelete = getattr(stampObject, 'onItemDelete', None) if onItemDelete is not None: onItemDelete(view, deferring) super(ContentItem, self).onItemDelete(view, deferring) def addToCollection(self, collection): """Add self to the given collection. For most items, just call collection.add(self), but for recurring events, this method is intercepted by a proxy and buffered while the user selects from various possible meanings for adding a recurring event to a collection. """ collection.add(self) def removeFromCollection(self, collection): """Remove self from the given collection. For most items, just call collection.remove(self), but for recurring events, this method is intercepted by a proxy and buffered while the user selects from various possible meanings for removing a recurring event from a collection. The special mine collection behavior that removed items should remain in the Dashboard is implemented here. """ self._prepareToRemoveFromCollection(collection) collection.remove(self) def _prepareToRemoveFromCollection(self, collection): """ If the collection is a mine collection and the item doesn't exist in any other 'mine' collections, manually add it to 'all' to keep the item 'mine'. We don't want to do this blindly though, or all's inclusions will get unnecessarily full. We also don't want to remove collection from mine.sources. That will cause a notification storm as items temporarily leave and re-enter being 'mine'. """ pim_ns = schema.ns('osaf.pim', self.itsView) mine = pim_ns.mine allCollection = pim_ns.allCollection if collection in mine.sources: for otherCollection in self.appearsIn: if otherCollection is collection: continue if otherCollection in mine.sources: # we found it in another 'mine' break else: # we didn't find it in a 'mine' Collection self.collections.add(allCollection) def getMembershipItem(self): """ Get the item that should be used to test for membership tests i.e. if item in collection: should be if item.getMembershipItem() in collection For most items, this is just itself, but for recurring events, this method is intercepted by a proxy. """ return self def changeEditState(self, modType=Modification.edited, who=None, when=None): """ @param modType: What kind of modification you are making. Used to set the value of C{self.lastModification}. @type modType: C{Modification} @param who: May be C{None}, which is interpreted as an anonymous user (e.g. a "drive-by" sharing user). Used to set the value of {self.lastModifiedBy}. @type who: C{EmailAddress} @param when: The date&time of this change. Used to set the value of C{self.lastModified}. The default, C{None}, sets the @type when: C{datetime} """ logger.debug( "ContentItem.changeEditState() self=%s view=%s modType=%s who=%s when=%s", self, self.itsView, modType, who, when) currentModFlags = self.modifiedFlags if (modType == Modification.edited and not self.hasLocalAttributeValue('lastModification', None) and not getattr(self, 'inheritFrom', self).hasLocalAttributeValue('lastModification')): # skip edits until the item is explicitly marked created return if modType == Modification.sent: if Modification.sent in currentModFlags: #raise ValueError, "You can't send an item twice" pass elif Modification.queued in currentModFlags: currentModFlags.remove(Modification.queued) elif modType == Modification.updated: #XXX Brian K: an update can occur with out a send. # Case user a sends a item to user b (state == sent) # user a edits item to user b and adds user c (state == update). # For user c the state of sent was never seen. #if not Modification.sent in currentModFlags: # raise ValueError, "You can't update an item till it's been sent" if Modification.queued in currentModFlags: currentModFlags.remove(Modification.queued) # Clear the edited flag and error on send/update/queue if (modType in (Modification.sent, Modification.updated, Modification.queued)): if Modification.edited in currentModFlags: currentModFlags.remove(Modification.edited) del self.error if not currentModFlags: self.modifiedFlags = set([modType]) else: currentModFlags.add(modType) self.lastModification = modType self.lastModified = when or datetime.now(self.itsView.tzinfo.default) self.lastModifiedBy = who # None => me """ ACCESSORS Accessors for Content Item attributes """ def getEmailAddress(self, nameOrAddressString): """ Lookup or create an EmailAddress based on the supplied string. This method is here for convenient access, so users don't need to import Mail. """ from mail import EmailAddress return EmailAddress.getEmailAddress(self.itsView, nameOrAddressString) def getCurrentMeEmailAddress(self): """ Lookup or create a current "me" EmailAddress. This method is here for convenient access, so users don't need to import Mail. """ import mail return mail.getCurrentMeEmailAddress(self.itsView) def getMyModifiedByAddress(self): """ Get an EmailAddress that represents the local user, for storing in lastModifiedBy after a local change. """ me = self.getCurrentMeEmailAddress() if not me: # Email not configured... # Get the user name associated with the default sharing account import osaf.sharing # hmm, this import seems wrong sharingAccount = osaf.sharing.getDefaultAccount(self.itsView) if sharingAccount is not None: import mail me = mail.EmailAddress.getEmailAddress(self.itsView, sharingAccount.username) return me def _updateCommonAttribute(self, attributeName, sourceAttributeName, collectorMethodName, args=()): """ Mechanism for coordinating updates to a common-display field (like displayWho and displayDate, but not displayName for now). """ if self.isDeleted(): return logger.debug("Collecting relevant %ss for %r %s", attributeName, self, self) collectorMethod = getattr(type(self), collectorMethodName) # Collect possible values. The collector method adds tuples to the # contenders list; each tuple starts with a value to sort by, and # ends with the attribute value to assign and the name of the attribute # it came from (which will be used later to pick a displayable string # to describe the source of the value). # # Examples: if we sort by the attribute value, only two values are # needed in the tuple: eg, (aDate, 'dateField). If the picking happens # based on an additional value (or values), the sort value(s) come # before the attribute value: eg (1, '*****@*****.**', 'to'), # (2, '*****@*****.**', 'from'). This works because sort sorts by the # values in order, and we access the attribute value and name using # negative indexes (so contender[-2] is the value, contender[-1] is # the name). contenders = [] collectorMethod(self, contenders, *args) # Now that we have the contenders, pick one. contenderCount = len(contenders) if contenderCount == 0: # No contenders: delete the value if hasattr(self, attributeName): delattr(self, attributeName) if hasattr(self, sourceAttributeName): delattr(self, sourceAttributeName) logger.debug("No relevant %s for %r %s", attributeName, self, self) return if contenderCount > 1: # We have more than one possibility: sort, then we'll use the first one. contenders.sort() result = contenders[0] logger.debug("Relevant %s for %r %s is %s", attributeName, self, self, result) assert result[-2] is not None setattr(self, attributeName, result[-2]) setattr(self, sourceAttributeName, result[-1]) if getattr(self, 'inheritFrom', None) is None: for item in getattr(self, 'inheritTo', []): item._updateCommonAttribute(attributeName, sourceAttributeName, collectorMethodName, args) def addDisplayWhos(self, whos): pass def updateDisplayWho(self, op, attr): self._updateCommonAttribute('displayWho', 'displayWhoSource', 'addDisplayWhos') def addDisplayDates(self, dates, now): super(ContentItem, self).addDisplayDates(dates, now) # Add our creation and last-mod dates, if they exist. for importance, attr in (999, "lastModified"), (1000, "createdOn"): v = getattr(self, attr, None) if v is not None: dates.append((importance, v, attr)) def updateDisplayDate(self, op, attr): now = datetime.now(tz=self.itsView.tzinfo.default) self._updateCommonAttribute('displayDate', 'displayDateSource', 'addDisplayDates', [now]) @schema.observer(modifiedFlags, lastModified, createdOn) def onCreatedOrLastModifiedChanged(self, op, attr): self.updateDisplayDate(op, attr) @schema.observer(modifiedFlags, lastModification, lastModifiedBy, read) def onModificationChange(self, op, name): # CommunicationStatus might have changed self.updateDisplayWho(op, name) @schema.observer(error) def onErrorChanged(self, op, attr): # Pop to now if error is anything non-False-ish (a non-empty # string, etc) if getattr(self, 'error', None): self.setTriageStatus(None, popToNow=True, force=True) def getBasedAttributes(self, attribute): """ Determine the schema attributes that affect this attribute (which might be a Calculated attribute) """ # If it's Calculated, see what it's based on; # otherwise, just return a list containing its own name. descriptor = getattr(self.__class__, attribute, None) try: basedOn = descriptor.basedOn except AttributeError: return (attribute, ) else: return tuple(desc.name for desc in basedOn) def isAttributeModifiable(self, attribute): """ Determine if an item's attribute is modifiable based on the shares it's in """ from osaf.sharing import isReadOnly return not isReadOnly(self)
class BranchPointBlock(BoxContainer): """ A block that can swap in different sets of child blocks (branch point "subtrees") based on its detailContents. It uses a BranchPointDelegate to do the heavy lifting. """ colorStyle = schema.One(ColorStyle) delegate = schema.One(required=True) detailItem = schema.One(schema.Item, defaultValue=None, inverse=schema.Sequence()) detailItemCollection = schema.One(schema.Item, defaultValue=None, inverse=schema.Sequence()) selectedItem = schema.One(schema.Item, defaultValue=None, inverse=schema.Sequence()) setFocus = schema.One(schema.Boolean, defaultValue=False) schema.addClouds(copying=schema.Cloud( byRef=[delegate, colorStyle, detailItem, selectedItem])) def instantiateWidget(self): return wxBranchPointBlock(self.parentBlock.widget) def onSelectItemsEvent(self, event): # for the moment, multiple selection means, "select nothing" # i.e. multiple selection in the summary view means selecting # nothing in the detail view # eventually we might want selectedItem to be an iterable # of some kind items = event.arguments['items'] if len(items) == 1: self.selectedItem = items[0] else: self.selectedItem = None self.detailItemCollection = self.delegate.getContentsCollection( self.selectedItem, event.arguments.get('collection')) widget = getattr(self, 'widget', None) if widget is not None: # eventually results in installTreeOfBlocks() widget.wxSynchronizeWidget() def installTreeOfBlocks(self, hints): """ If necessary, replace our children with a tree of blocks appropriate for our content. Four steps: 1) map the selected item to a cache key 2) Use the cache key to get the appropriate tree of blocks 3) Set contents on that new tree of blocks 4) Render the tree of blocks """ # Get a cache key from self.selectedItem using the delegate keyItem = self.delegate._mapItemToCacheKeyItem(self.selectedItem, hints) # Ask the delegate for the right tree of blocks # (actually there is only implmentation of this function, # should probably be rolled into BranchParentBlock eventually) newView = self.delegate.getBranchForKeyItem(keyItem) if keyItem is None: detailItem = None else: # Seems like we should always mark new views with an event boundary assert newView is None or newView.eventBoundary detailItem = self.delegate._getContentsForBranch( newView, self.selectedItem, keyItem) detailItemChanged = self.detailItem is not detailItem self.detailItem = detailItem # Set contents on the root of the tree of blocks # For bug 4269 in 0.6: If we've been given a contents collection, # it's so that we can put our detailItem in it, to get a notification # when that item is deleted. Update the collection if necessary. contents = getattr(self, 'contents', None) if (contents is not None and contents.first() is not detailItem): contents.clear() if detailItem is not None: contents.add(self.detailItem) oldView = self.childBlocks.first() treeChanged = newView is not oldView logger.debug( "installTreeOfBlocks %s: treeChanged=%s, detailItemChanged=%s, detailItem=%s", debugName(self), treeChanged, detailItemChanged, debugName(detailItem)) # Render or rerender as necessary if treeChanged: # get rid of the old view if oldView is not None: # We need to get rid of our sizer that refers to widgets # that are going to get deleted assert hasattr(self, "widget") self.widget.SetSizer(None) oldView.unRender() # attach the new view self.childBlocks = [newView] if newView is not None else [] if newView is not None: def Rerender(): if (detailItemChanged or treeChanged or hints.get("sendSetContents", False)): newView.postEventByName( "SetContents", { 'item': detailItem, 'collection': self.detailItemCollection }) if not hasattr(newView, "widget"): newView.render() elif detailItemChanged or treeChanged: resyncMethod = getattr(newView, 'synchronizeWidgetDeep', None) if resyncMethod is not None: resyncMethod() if self.setFocus: newView.postEventByName("SetFocus", {}) treeController = getattr(newView, "treeController", None) if treeController is not None: treeController.initializeTree(hints) IgnoreSynchronizeWidget(False, Rerender)
class Contact(items.ContentItem): """An entry in an address book. Typically represents either a person or a company. * issue: We might want to keep track of lots of sharing information like 'Permissions I've given them', 'Items of mine they've subscribed to', 'Items of theirs I've subscribed to', etc. """ schema.kindInfo(displayName="Contact", displayAttribute="emailAddress") itemsCreated = schema.Sequence( displayName="Items Created", doc="List of content items created by this user.", inverse=items.ContentItem.creator, ) contactName = schema.One(ContactName, inverse=ContactName.contact, initialValue=None) emailAddress = schema.One(schema.String, displayName="Email Address", initialValue="") itemsLastModified = schema.Sequence( items.ContentItem, displayName="Items Last Modified", doc="List of content items last modified by this user.", inverse=items.ContentItem.lastModifiedBy) requestedTasks = schema.Sequence( "osaf.pim.tasks.TaskMixin", displayName="Requested Tasks", doc="List of tasks requested by this user.", inverse="requestor") taskRequests = schema.Sequence( "osaf.pim.tasks.TaskMixin", displayName="Task Requests", doc="List of tasks requested for this user.", otherName="requestee" # XXX other end points to ContentItem??? ) organizedEvents = schema.Sequence( "osaf.pim.calendar.Calendar.CalendarEventMixin", displayName="Organized Events", doc="List of events this user has organized.", inverse="organizer") participatingEvents = schema.Sequence( "osaf.pim.calendar.Calendar.CalendarEventMixin", displayName="Participating Events", doc="List of events this user is a participant.", inverse="participants") sharerOf = schema.Sequence( # Share displayName="Sharer Of", doc="List of shares shared by this user.", otherName="sharer") shareeOf = schema.Sequence( # Share displayName="Sharee Of", doc="List of shares for which this user is a sharee.", otherName="sharees") # <!-- redirections --> who = schema.Role(redirectTo="contactName") about = schema.Role(redirectTo="displayName") date = schema.Role(redirectTo="createdOn") schema.addClouds(sharing=schema.Cloud(emailAddress, byCloud=[contactName])) def __init__(self, name=None, parent=None, kind=None, view=None, **kw): super(Contact, self).__init__(name, parent, kind, view, **kw) # If I didn't get assigned a creator, then I must be the "me" contact # and I want to be my own creator: if self.creator is None: self.creator = self def InitOutgoingAttributes(self): """ Init any attributes on ourself that are appropriate for a new outgoing item. """ try: super(Contact, self).InitOutgoingAttributes() except AttributeError: pass self.contactName = ContactName() self.contactName.firstName = '' self.contactName.lastName = '' # Cache "me" for fast lookup; used by getCurrentMeContact() meContactID = None @classmethod def getCurrentMeContact(cls, view): """ Lookup the current "me" Contact """ # cls.meContactID caches the Contact representing the user. One will # be created if it doesn't yet exist. if cls.meContactID is not None: me = view.findUUID(cls.meContactID) if me is not None: return me # Our cached UUID is invalid cls.meContactID is None parent = cls.getDefaultParent(view) me = parent.getItemChild("me") if me is None: me = Contact(name="me", parent=parent, displayName="Me") me.contactName = ContactName(parent=parent, firstName="Chandler", lastName="User") cls.meContactID = me.itsUUID return me def getContactForEmailAddress(cls, view, address): """ Given an email address string, find (or create) a matching contact. @param view: The repository view object @type view: L{repository.persistence.RepositoryView} @param address: An email address to use for looking up a contact @type address: string @return: A Contact """ """ @@@MOR, convert this section to use Query; I tried briefly but wasn't successful, and it's just using KindQuery right now: query = Query.Query(view, parent=view.findPath("//userdata"), queryString="") # @@@MOR Move this to a singleton queryString = "for i in '//parcels/osaf/pim/contacts/Contact' where i.emailAddress == $0" query.args = { 0 : address } query.execute() for item in query: """ for item in cls.iterItems(view): if item.emailAddress == address: return item # Just return the first match # Need to create a new Contact contact = Contact(view=view) contact.emailAddress = address contact.contactName = None return contact getContactForEmailAddress = classmethod(getContactForEmailAddress) def __str__(self): """ User readable string version of this address. """ if self.isStale(): return super(Contact, self).__str__() # Stale items shouldn't go through the code below value = self.getItemDisplayName() return value
class RecurrenceRuleSet(items.ContentItem): """ A collection of recurrence and exclusion rules, dates, and exclusion dates. """ rrules = schema.Sequence(RecurrenceRule, inverse=RecurrenceRule.rruleFor, deletePolicy='cascade') exrules = schema.Sequence(RecurrenceRule, inverse=RecurrenceRule.exruleFor, deletePolicy='cascade') rdates = schema.Sequence(schema.DateTimeTZ, ) exdates = schema.Sequence(schema.DateTimeTZ, ) events = schema.Sequence() # inverse of EventStamp.rruleset schema.addClouds(copying=schema.Cloud(rrules, exrules, rdates, exdates), sharing=schema.Cloud(literal=[exdates, rdates], byCloud=[exrules, rrules])) @schema.observer(rrules, exrules, rdates, exdates) def onRuleSetChanged(self, op, name): """If the RuleSet changes, update the associated event.""" if not getattr(self, '_ignoreValueChanges', False): if self.hasLocalAttributeValue('events'): pimNs = schema.ns("osaf.pim", self.itsView) for eventItem in self.events: pimNs.EventStamp(eventItem).getFirstInRule().cleanRule() # assume we have only one conceptual event per rrule break def addRule(self, rule, rrulesorexrules='rrules'): """Add an rrule or exrule, defaults to rrule. @param rule: Rule to be added @type rule: L{RecurrenceRule} @param rrulesorexrules: Whether the rule is an rrule or exrule @type rrulesorexrules: 'rrules' or 'exrules' """ try: getattr(self, rrulesorexrules).append(rule) except AttributeError: setattr(self, rrulesorexrules, [rule]) def createDateUtilFromRule(self, dtstart, ignoreIsCount=True, convertFloating=False, ignoreShortFrequency=True): """Return an appropriate dateutil.rrule.rruleset. @param dtstart: The start time for the recurrence rule @type dtstart: C{datetime} @param ignoreIsCount: Whether the isCount flag should be used to convert until endtimes to a count. Converting to count takes extra cycles and is only necessary when the rule is going to be serialized @type ignoreIsCount: C{bool} @param convertFloating: Whether or not to allow view.tzinfo.floating in datetimes of the rruleset. If C{True}, naive datetimes are used instead. This is needed for exporting floating events to icalendar format. @type convertFloating: C{bool} @param ignoreShortFrequency: If ignoreShortFrequency is True, replace hourly or more frequent rules with a single RDATE matching dtstart, so as to avoid wasting millions of cycles on nonsensical recurrence rules. @type ignoreShortFrequency: C{bool} @rtype: C{dateutil.rrule.rruleset} """ view = self.itsView args = (ignoreIsCount, convertFloating) ruleset = rruleset() for rtype in 'rrule', 'exrule': for rule in getattr(self, rtype + 's', []): if ignoreShortFrequency and rule.freq in SHORT_FREQUENCIES: # too-frequent rule, as Admiral Ackbar said, "IT'S A TRAP!" ruleset.rdate(dtstart) return ruleset rule_adder = getattr(ruleset, rtype) rule_adder(rule.createDateUtilFromRule(dtstart, *args)) for datetype in 'rdate', 'exdate': for date in getattr(self, datetype + 's', []): if convertFloating and date.tzinfo == view.tzinfo.floating: date = date.replace(tzinfo=None) else: date = coerceTimeZone(view, date, dtstart.tzinfo) getattr(ruleset, datetype)(date) if (ignoreIsCount and not getattr(self, 'rrules', []) and getattr(self, 'rdates', [])): # no rrule, but there are RDATEs, create an RDATE for dtstart, or it # won't appear to be part of the rule ruleset.rdate(dtstart) return ruleset def setRuleFromDateUtil(self, ruleSetOrRule): """Extract rules and dates from ruleSetOrRule, set them in self. If a dateutil.rrule.rrule is passed in instead of an rruleset, treat it as the new rruleset. @param ruleSetOrRule: The rule to marshall into Chandler @type ruleSetOrRule: C{dateutil.rrule.rrule} or C{dateutil.rrule.rruleset} """ ignoreChanges = getattr(self, '_ignoreValueChanges', False) self._ignoreValueChanges = True try: if isinstance(ruleSetOrRule, rrule): set = rruleset() set.rrule(ruleSetOrRule) ruleSetOrRule = set elif not isinstance(ruleSetOrRule, rruleset): raise TypeError, "ruleSetOrRule must be an rrule or rruleset" for rtype in 'rrule', 'exrule': rules = getattr(ruleSetOrRule, '_' + rtype, []) if rules is None: rules = [] itemlist = [] for rule in rules: ruleItem = RecurrenceRule(None, None, None, self.itsView) ruleItem.setRuleFromDateUtil(rule) itemlist.append(ruleItem) setattr(self, rtype + 's', itemlist) for typ in 'rdate', 'exdate': datetimes = [ forceToDateTime(self.itsView, d) for d in getattr(ruleSetOrRule, '_' + typ, []) ] setattr(self, typ + 's', datetimes) finally: self._ignoreValueChanges = ignoreChanges # only one rule cleaning is useful when setting a new rule self.onRuleSetChanged(None, None) def isComplex(self): """Determine if the rule is too complex to display a meaningful description about it. @rtype: C{bool} """ if hasattr(self, 'rrules'): if len(self.rrules) != 1: return True # multiple rules for recurtype in 'exrules', 'rdates': if self.hasLocalAttributeValue(recurtype) and \ len(getattr(self, recurtype)) != 0: return True # more complicated rules rule = self.rrules.first() for attr in RecurrenceRule.listNames: if getattr(rule, attr) not in (None, []): return True if rule.byweekday is not None: # treat nth weekday of the month as complex for daystruct in rule.byweekday: if daystruct.selector != 0: return True return False else: return True def isCustomRule(self): """Determine if this is a custom rule. For the moment, simple daily, weekly, or monthly repeating events, optionally with an UNTIL date, or the abscence of a rule, are the only rules which are not custom. @rtype: C{bool} """ if self.isComplex(): return True # isComplex has already tested for most custom things, but # not intervals greater than 1 and multiple weekdays rule = self.rrules.first() if not (rule.interval == 1 or (rule.interval == 2 and rule.freq == 'weekly')): return True elif rule.isWeekdayRule(): return False elif rule.byweekday: return True else: return False def getCustomDescription(self): """Return a string describing custom rules. @rtype: C{str} """ too_frequent = False if hasattr(self, 'rrules') and len(self.rrules) > 0: for rule in self.rrules: if rule.freq in SHORT_FREQUENCIES: too_frequent = True break if self.isComplex(): if not too_frequent: return _(u"No description.") else: return _(u"Too frequent.") else: # Get the rule values we can interpret (so far...) rule = self.rrules.first() freq = rule.freq interval = rule.interval until = rule.calculatedUntil() days = rule.byweekday # Build the index and argument dictionary # The index must be built in the 'fxdus' order index = 'f' dct = {} if too_frequent: index += 'x' elif freq == 'weekly' and days is not None: index += 'd' daylist = [weekdayAbbrevMap[i.weekday] for i in days] dct['days'] = u" ".join(daylist) dct['frequencyplural'] = pluralFrequencyMap[freq] dct['frequencysingular'] = singularFrequencyMap[freq] dct['frequencyadverb'] = adverbFrequencyMap[freq] if not too_frequent and until is not None: index += 'u' formatter = DateFormat.createDateInstance(DateFormat.kShort) dct['date'] = unicode(formatter.format(until)) if interval != 1: dct['interval'] = str(interval) index += 'p' else: index += 's' return descriptionFormat[index] % dct def transformDatesAfter(self, after, changeDate): """ Transform dates (later than "after") in exdates, rdates and until, by applying the function C{changeDate}. @param after: Earliest date to move, or None for all occurrences @type after: C{datetime} or None @param changeDate: Time difference @type changeDate: C{callable}, taking a C{datetime} and returning a C{datetime} """ self._ignoreValueChanges = True for datetype in 'rdates', 'exdates': datelist = getattr(self, datetype, None) if datelist is not None: l = [] for dt in datelist: if after is None or dt >= after: l.append(changeDate(dt)) else: l.append(dt) setattr(self, datetype, l) for rule in self.rrules or []: if not rule.untilIsDate: try: until = rule.until except AttributeError: pass else: if after is None or until >= after: rule.until = changeDate(until) del self._ignoreValueChanges def removeDates(self, cmpFn, endpoint): """Remove dates in exdates and rdates before or after endpoint. @param cmpFn: Comparison function (will be called with two C{datetime} objects as arguments). @type cmpFn: callable @param endpoint: Start or end point for comparisons @type endpoint: C{datetime} """ for datetype in 'rdates', 'exdates': datelist = getattr(self, datetype, None) if datelist is not None: # need to start from the end, bug 11005 for i in reversed(xrange(len(datelist))): if cmpFn(datelist[i], endpoint): self._ignoreValueChanges = True del datelist[i] self._ignoreValueChanges = False def moveRuleEndBefore(self, dtstart, end): """Make self's rules end before end. @param dtstart: Start time for the recurrence rule @type dtstart: C{datetime} @param end: Date not to include in the rule's new end @type end: C{datetime} """ #change the rule, onRuleSetChanged will trigger cleanRule for master for rule in getattr(self, 'rrules', []): if (not rule.hasLocalAttributeValue('until') or (rule.calculatedUntil() >= end)): rule.moveUntilBefore(dtstart, end) self.removeDates(datetime.__ge__, end)
class PhotoMixin(items.ContentItem): schema.kindInfo(displayName="Photo Mixin Kind", displayAttribute="displayName") dateTaken = schema.One(schema.DateTime, displayName="taken") file = schema.One(schema.String) exif = schema.Mapping(schema.String, initialValue={}) photoBody = schema.One(schema.Lob) about = schema.One(redirectTo='displayName') date = schema.One(redirectTo='dateTaken') who = schema.One(redirectTo='creator') schema.addClouds(sharing=schema.Cloud(dateTaken, photoBody)) def importFromFile(self, path): data = file(path, "rb").read() (mimeType, encoding) = mimetypes.guess_type(path) self.photoBody = utils.dataToBinary(self, 'photoBody', data, mimeType=mimeType) def importFromURL(self, url): if isinstance(url, URL): url = str(url) data = urllib.urlopen(url).read() (mimeType, encoding) = mimetypes.guess_type(url) self.photoBody = utils.dataToBinary(self, 'photoBody', data, mimeType=mimeType) def exportToFile(self, path): data = utils.binaryToData(self.photoBody) out = file(path, "wb") out.write(data) out.close() def processEXIF(self): input = self.photoBody.getInputStream() data = input.read() input.close() stream = cStringIO.StringIO(data) try: exif = EXIF.process_file(stream) # Warning, serious nesting ahead self.dateTaken = datetime.datetime.fromtimestamp( time.mktime( time.strptime(str(exif['Image DateTime']), "%Y:%m:%d %H:%M:%S"))) self.exif = {} for (key, value) in exif.iteritems(): if isinstance(value, EXIF.IFD_Tag): self.exif[key] = value.printable else: self.exif[key] = value except Exception, e: logger.debug("Couldn't process EXIF of Photo %s (%s)" % \ (self.itsPath, e))
class RSSChannel(ItemCollection): schema.kindInfo(displayName="RSS Channel") link = schema.One( schema.URL, displayName="link" ) category = schema.One( schema.String, displayName="Category" ) author = schema.One( schema.String, displayName="Author" ) date = schema.One( schema.DateTime, displayName="Date" ) url = schema.One( schema.URL, displayName="URL" ) etag = schema.One( schema.String, displayName="eTag" ) lastModified = schema.One( schema.DateTime, displayName="Last Modified" ) copyright = schema.One( schema.String, displayName="Copyright" ) language = schema.One( schema.String, displayName="Language" ) isUnread = schema.One( schema.Boolean, displayName="Is Unread" ) schema.addClouds( sharing = schema.Cloud(author, copyright, link, url) ) who = schema.Role(redirectTo="author") about = schema.Role(redirectTo="about") def Update(self, data=None): logger.info("Updating channel: %s" % getattr(self, 'displayName', self.url)) etag = self.getAttributeValue('etag', default=None) lastModified = self.getAttributeValue('lastModified', default=None) if lastModified: lastModified = lastModified.timetuple() if not data: # fetch the data data = feedparser.parse(str(self.url), etag, lastModified) # set etag SetAttribute(self, data, 'etag') # set lastModified modified = data.get('modified') if modified: self.lastModified = datetime.datetime.fromtimestamp(time.mktime(modified)).replace(tzinfo=None) # if the feed is bad, raise the sax exception try: if data.bozo and not isinstance(data.bozo_exception, feedparser.CharacterEncodingOverride): logger.error("For url '%s', feedparser exception: %s" % (self.url, data.bozo_exception)) raise data.bozo_exception except KeyError: print "Error" return self._DoChannel(data['channel']) count = self._DoItems(data['items']) if count: logger.info("...added %d RSSItems" % count) def addRSSItem(self, rssItem): """ Add a single item, and add it to any listening collections """ rssItem.channel = self self.add(rssItem) def _DoChannel(self, data): # fill in the item attrs = {'title':'displayName'} SetAttributes(self, data, attrs) attrs = ['link', 'description', 'copyright', 'category', 'language'] # @@@MOR attrs = ['link', 'description', 'copyright', 'creator', 'category', 'language'] SetAttributes(self, data, attrs) date = data.get('date') if date: self.date = parse(str(date)) def _DoItems(self, items): # make children # lets look for each existing item. This is ugly and is an O(n^2) problem # if the items are unsorted. Bleah. view = self.itsView count = 0 for newItem in items: # Convert date to datetime object try: itemDate = newItem.date except: itemDate = None if itemDate: newItem.date = parse(str(itemDate)) else: # Give the item a date so we can sort on it newItem.date = datetime.datetime.now() # Disregard timezone for now newItem.date = newItem.date.replace(tzinfo=None) found = False for oldItem in self.resultSet: # check to see if this doesn't already exist if oldItem.isSimilar(newItem): found = True oldItem.Update(newItem) break if not found: # we have a new item - add it rssItem = RSSItem(view=view) rssItem.Update(newItem) try: self.addRSSItem(rssItem) count += 1 except: logger.error("Error adding an RSS item") return count def markAllItemsRead(self): for item in self: item.isRead = True
class RecurrenceRuleSet(items.ContentItem): rrules = schema.Sequence(RecurrenceRule, displayName="Recurrence rules", inverse=RecurrenceRule.rruleFor, deletePolicy='cascade') exrules = schema.Sequence(RecurrenceRule, displayName="Exclusion rules", inverse=RecurrenceRule.exruleFor, deletePolicy='cascade') rdates = schema.Sequence(schema.DateTime, displayName="Recurrence Dates") exdates = schema.Sequence(schema.DateTime, displayName="Exclusion Dates") events = schema.Sequence("osaf.pim.calendar.Calendar.CalendarEventMixin", displayName="Events", inverse="rruleset") schema.addClouds(copying=schema.Cloud(rrules, exrules, rdates, exdates)) def addRule(self, rule, rruleorexrule='rrule'): """Add an rrule or exrule, defaults to rrule.""" rulelist = getattr(self, rruleorexrule + 's', []) rulelist.append(rule) setattr(self, rruleorexrule + 's', rulelist) def createDateUtilFromRule(self, dtstart): """Return an appropriate dateutil.rrule.rruleset.""" ruleset = rruleset() for rtype in 'rrule', 'exrule': for rule in getattr(self, rtype + 's', []): getattr(ruleset, rtype)(rule.createDateUtilFromRule(dtstart)) for datetype in 'rdate', 'exdate': for date in getattr(self, datetype + 's', []): getattr(ruleset, datetype)(date) return ruleset def setRuleFromDateUtil(self, ruleSetOrRule): """Extract rules and dates from ruleSetOrRule, set them in self. If a dateutil.rrule.rrule is passed in instead of an rruleset, treat it as the new rruleset. """ if isinstance(ruleSetOrRule, rrule): set = rruleset() set.rrule(ruleSetOrRule) ruleSetOrRule = set elif not isinstance(ruleSetOrRule, rruleset): raise TypeError, "ruleSetOrRule must be an rrule or rruleset" for rtype in 'rrule', 'exrule': rules = getattr(ruleSetOrRule, '_' + rtype, []) if rules is None: rules = [] itemlist = [] for rule in rules: ruleItem = RecurrenceRule(parent=self) ruleItem.setRuleFromDateUtil(rule) itemlist.append(ruleItem) setattr(self, rtype + 's', itemlist) for typ in 'rdate', 'exdate': # While most dates are naive, strip tzinfo off naive = [stripTZ(e) for e in getattr(ruleSetOrRule, '_' + typ, [])] setattr(self, typ + 's', naive) def isCustomRule(self): """Determine if this is a custom rule. For the moment, simple daily, weekly, or monthly repeating events, optionally with an UNTIL date, or the abscence of a rule, are the only rules which are not custom. """ if self.hasLocalAttributeValue('rrules'): if len(self.rrules) > 1: return True # multiple rules for recurtype in 'exrules', 'rdates', 'exdates': if self.hasLocalAttributeValue(recurtype) and \ len(getattr(self, recurtype)) != 0: return True # more complicated rules rule = list(self.rrules)[0] if rule.interval != 1: return True for attr in RecurrenceRule.listNames + ("byweekday", ): if getattr(rule, attr): return True return False def getCustomDescription(self): """Return a string describing custom rules.""" return "not yet implemented" def onValueChanged(self, name): """If the RuleSet changes, update the associated event.""" if name in ('rrules', 'exrules', 'rdates', 'exdates'): if self.hasLocalAttributeValue('events'): for event in self.events: event.getFirstInRule().cleanRule() # assume we have only one conceptual event per rrule break
class TaskMixin(items.ContentItem): """This is the set of Task-specific attributes. Task Mixin is the bag of Task-specific attributes. We only instantiate these Items when we "unstamp" an Item, to save the attributes for later "restamping". """ schema.kindInfo( displayName = "Task Mixin Kind", description = "This Kind is 'mixed in' to others kinds to create Kinds that " "can be instantiated" ) recurrence = schema.Sequence( displayName = 'Recurrence Patterns', doc = 'This is a placeholder and probably not used for 0.5', ) reminderTime = schema.One( schema.DateTime, displayName = 'ReminderTime', doc = 'This may not be general enough', ) requestor = schema.One( Contact, displayName = 'Requestor', issues = [ 'Type could be Contact, EmailAddress or String', 'Think about using the icalendar terminology' ], inverse = Contact.requestedTasks, ) requestee = schema.Sequence( items.ContentItem, displayName = 'Requestee', issues = [ 'Type could be Contact, EmailAddress or String', 'Think about using the icalendar terminology' ], otherName = 'taskRequests', ) taskStatus = schema.One( TaskStatusEnum, displayName = 'Task Status', ) dueDate = schema.One(schema.DateTime, displayName = 'Due date') who = schema.One(redirectTo = 'requestee') whoFrom = schema.One(redirectTo = 'requestor') about = schema.One(redirectTo = 'displayName') # XXX these two links should probably point to TaskMixin instead of # Task, because as-is they won't support stamping. Note that if # this is corrected, the opposite ends should be set using 'inverse' # instead of 'otherName'. dependsOn = schema.Sequence( 'Task', displayName = 'Depends On', otherName = 'preventsProgressOn', ) preventsProgressOn = schema.Sequence( 'Task', displayName = 'Blocks', otherName = 'dependsOn', ) schema.addClouds( copying = schema.Cloud( requestor, requestee, dependsOn, preventsProgressOn ) ) def InitOutgoingAttributes (self): """ Init any attributes on ourself that are appropriate for a new outgoing item. """ try: super(TaskMixin, self).InitOutgoingAttributes () except AttributeError: pass TaskMixin._initMixin (self) # call our init, not the method of a subclass def _initMixin (self): """ Init only the attributes specific to this mixin. Called when stamping adds these attributes, and from __init__ above. """ # default status is To Do self.taskStatus = 'todo' # default due date is 1 hour hence self.dueDate = datetime.now() + timedelta(hours=1) # default the title to any super class "about" definition try: self.about = self.getAnyAbout () except AttributeError: pass # default the requestor to any super class "whoFrom" definition try: whoFrom = self.getAnyWhoFrom () # I only want a Contact if not isinstance(whoFrom, Contact): whoFrom = self.getCurrentMeContact(self.itsView) self.requestor = whoFrom except AttributeError: pass """ @@@ Commenting out this block requestee can only accept Contact items. At some point this code will need inspect the results of getAnyWho() and create Contact items for any EmailAddresses in the list # default the requestee to any super class "who" definition try: shallow copy the list self.requestee = self.getAnyWho () except AttributeError: pass @@@ End block comment """ def getAnyDate (self): """ Get any non-empty definition for the "date" attribute. """ # @@@ Don't do this for now, per bug 2654; will be revisited in 0.6. """ try: return self.dueDate except AttributeError: pass """ return super (TaskMixin, self).getAnyDate () def getAnyWho (self): """ Get any non-empty definition for the "who" attribute. """ try: return self.requestee except AttributeError: pass return super (TaskMixin, self).getAnyWho () def getAnyWhoFrom (self): """ Get any non-empty definition for the "whoFrom" attribute. """ try: return self.requestor except AttributeError: pass return super (TaskMixin, self).getAnyWhoFrom ()