class Startup(schema.Item): """Subclass this & create parcel.xml instances for startup notifications""" invoke = schema.One( schema.String, doc="Full name of a class or function to import and run at startup") active = schema.One( schema.Boolean, doc="Set to False to disable invocation of this item at startup", initialValue=True, ) requires = schema.Sequence("Startup", doc="Startups that must run before this one", initialValue=[]) requiredBy = schema.Sequence(inverse=requires) def getTarget(self): """Import the object named by ``invoke`` and return it""" return schema.importString(self.invoke) def invokeTarget(self, target): """Run the specified startup target in the current thread""" target(self) def onStart(self): """Override this method in a subclass to receive the notification Note: you should *not* create or modify items in this method or code called from this method. If you want to do that, you probably don't want to be using a Startup item. Also note that you should not call invoke this method via super() unless you want the default behavior (i.e., importing and running the ``invoke`` attribute) to occur. """ self.invokeTarget(self.getTarget()) def _start(self, attempted, started): """Handle inter-startup ordering and invoke onStart()""" if self in started: return True elif not self.active or self in attempted: return False attempted.add(self) # prevent multiple attempts to start this item canStart = True for item in self.requires: canStart = canStart and item._start(attempted, started) if canStart: self.onStart() started.add(self) return True return False
class BranchSubtree(schema.Annotation): """ A mapping between an Item and the list of top-level blocks ('rootBlocks') that should appear when an Item inheriting from that Kind is displayed. Each rootBlock entry should have its 'position' attribute specified, to enable it to be sorted with other root blocks.) """ schema.kindInfo(annotates=schema.Kind) rootBlocks = schema.Sequence(Block, inverse=schema.Sequence())
class ColumnHeader(RectangularChild): """This class defines a generic ColumnHeader kind.""" # -- Attributes for ColumnHeader -- # columnHeadings = schema.Sequence(schema.Text, required=True) columnWidths = schema.Sequence(schema.Integer) proportionalResizing = schema.One(schema.Boolean) visibleSelection = schema.One(schema.Boolean) genericRenderer = schema.One(schema.Boolean) # add selection attribute? def ResizeHeader(self): for (i, width) in enumerate(self.columnWidths): if hasattr(self, "widget"): self.widget.SetItemSize(i, (width, 0)) def defaultOnSize(self, event): self.ResizeHeader() event.Skip() def instantiateWidget(self): # create widget instance as a child of the parent block's widget. wxColHeaderInstance = colheader.ColumnHeader(self.parentBlock.widget) # FYI: currently, calendar needs proportional resizing off (false), because sizing needs to be exact wxColHeaderInstance.SetAttribute( colheader.CH_ATTR_ProportionalResizing, self.proportionalResizing) # set attributes if hasattr(self, "visibleSelection"): wxColHeaderInstance.SetAttribute( colheader.CH_ATTR_VisibleSelection, self.visibleSelection) if hasattr(self, "proportionalResizing "): wxColHeaderInstance.SetAttribute( colheader.CH_ATTR_ProportionalResizing, self.proportionalResizing) if hasattr(self, "genericRenderer"): wxColHeaderInstance.SetAttribute(colheader.CH_ATTR_GenericRenderer, self.genericRenderer) # add columns. for header in self.columnHeadings: wxColHeaderInstance.AppendItem(header, wx.ALIGN_CENTER, 20, bSortEnabled=False) # set a default size-event handler (this may need to be removed) wxColHeaderInstance.Bind(wx.EVT_SIZE, self.defaultOnSize) wxColHeaderInstance.Layout() return wxColHeaderInstance
class FilteredCollection(SingleSourceWrapperCollection): """ A ContentCollection which is the result of applying a boolean predicate to every item of another ContentCollection. The C{source} attribute contains the ContentCollection instance to be filtered. The C{filterExpression} attribute is a string containing a Python expression. If the expression returns C{True} for an item in the C{source} it will be in the FilteredCollection. The C{filterAttributes} attribute is a list of attribute names (Strings), which are accessed by the C{filterExpression}. Failure to provide this list will result in missing notifications. """ filterExpression = schema.One(schema.Text) filterMethod = schema.One(schema.Tuple) filterAttributes = schema.Sequence(schema.Importable, initialValue=[]) def _sourcesChanged_(self, op): source = self.source if source is None: s = EmptySet() else: attrs = tuple(set(self.filterAttributes)) if hasattr(self, 'filterExpression'): s = ExpressionFilteredSet(source, self.filterExpression, attrs) else: s = MethodFilteredSet(source, self.filterMethod, attrs) setattr(self, self.__collection__, s) return s
class Project(ContentItem): schema.kindInfo( displayName="Project", examples=[ 'my "Housewarming Party" project', "my department's \"Move to new building\" project", "my company's \"Open Seattle Store\" project", ], description= "Users can create projects to help organize their work. Users can " "take content items (like tasks and mail messages) and assign " "them to different projects.") parentProject = schema.One( 'Project', displayName='Parent Project', doc= 'Projects can be organized into hierarchies. Each project can have one parent.', inverse='subProjects', ) subProjects = schema.Sequence( 'Project', displayName='Sub Projects', doc= 'Projects can be organized into hierarchies. Each project can have many sub-projects.', inverse='parentProject', )
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 Account(schema.Item): userid = schema.One(schema.Text) server = schema.One(schema.Importable) protocol = schema.One(schema.Symbol) user = schema.One(otherName='accounts', initialValue=None) default = schema.One(schema.Boolean, initialValue=False) autoLogin = schema.One(schema.Boolean, initialValue=False) conduits = schema.Sequence(otherName='account') _clients = {} schema.initialValues( user=lambda self: User(itsView=self.itsView, name=self.userid)) def login(self, printf, autoLogin=False): raise NotImplementedError, "%s.login" % (type(self)) def isLoggedIn(self): raise NotImplementedError, "%s.isLoggedIn" % (type(self)) def subscribe(self, name, remoteId): raise NotImplementedError, "%s.subscribe" % (type(self)) def sync(self, share): raise NotImplementedError, "%s.sync" % (type(self)) def _getClient(self): return type(self)._clients.get(self.itsUUID) def _setClient(self, client): type(self)._clients[self.itsUUID] = client client = property(_getClient, _setClient)
class Location(ContentItem): """ @note: Location may not be calendar specific. """ schema.kindInfo(displayName="Location", displayAttribute="displayName") eventsAtLocation = schema.Sequence(CalendarEventMixin, displayName="Calendar Events", inverse=CalendarEventMixin.location) def __str__(self): """ User readable string version of this Location """ if self.isStale(): return super(Location, self).__str__() # Stale items can't access their attributes return self.getItemDisplayName() def getLocation(cls, view, locationName): """ Factory Method for getting a Location. Lookup or create a Location based on the supplied name string. If a matching Location object is found in the repository, it is returned. If there is no match, then a new item is created and returned. @param locationName: name of the Location @type locationName: C{String} @return: C{Location} created or found """ # make sure the locationName looks reasonable assert locationName, "Invalid locationName passed to getLocation factory" # get all Location objects whose displayName match the param its = Location.iterItems(view, exact=True) locQuery = [i for i in its if i.displayName == locationName] ## locQuery = view.findPath('//Queries/calendarLocationQuery') ## if locQuery is None: ## queryString = u'for i in "//parcels/osaf/pim/calendar/Location" \ ## where i.displayName == $0' ## p = view.findPath('//Queries') ## k = view.findPath('//Schema/Core/Query') ## locQuery = Query.Query ('calendarLocationQuery', p, k, queryString) ## locQuery.args["$0"] = ( locationName, ) # return the first match found, if any for firstSpot in locQuery: return firstSpot # make a new Location newLocation = Location(view=view) newLocation.displayName = locationName return newLocation getLocation = classmethod(getLocation)
class AllIndexDefinitions(schema.Item): """ Singleton item that hosts a reflist of all L{IndexDefinition}s in existance: L{IndexDefinition}'s constructor adds each new instance to us to assure this. """ indexDefinitions = schema.Sequence(IndexDefinition, initialValue=[], inverse=schema.One())
class TrunkSubtree(schema.Item): """A mapping between an Item and the "root blocks" that should appear when an Item inheriting from that Kind is displayed. (A "root block" should have a "position" attribute to enable it to be sorted with other root blocks.) """ key = schema.One(schema.Item, required=True) rootBlocks = schema.Sequence('Block', inverse='parentTrunkSubtrees')
class Principal(ContentItem): # @@@MOR These should be moved out so that authentication can be made # more general, but they're here for convenience for now. login = schema.One(schema.Text) password = schema.One(schema.Text) members = schema.Sequence(initialValue=[]) memberOf = schema.Sequence(initialValue=[], inverse=members) def isMemberOf(self, pid): if self.itsUUID == pid: return True for member in getattr(self.itsView.findUUID(pid), 'members', []): if self.isMemberOf(member.itsUUID): return True return False
class TaskEventExtraMixin(items.ContentItem): """ Task Event Extra Mixin is the bag of attributes that appears when you have an Item that is both a Task and a CalendarEvent. We only instantiate these Items when we "unstamp" an Item, to save the attributes for later "restamping". """ schema.kindInfo( displayName = "Task Event Extra Mixin Kind", description = "The attributes specific to an item that is both a task and an " "event. This is additional 'due by' information. " ) dueByDate = schema.One( schema.DateTime, displayName = 'Due by Date', doc = 'The date when a Task Event is due.', ) dueByRecurrence = schema.Sequence( 'osaf.pim.calendar.Calendar.RecurrencePattern', displayName = 'Due by Recurrence', doc = 'Recurrence information for a Task Event.', ) dueByTickler = schema.One( schema.DateTime, displayName = 'Due by Tickler', doc = 'The reminder information for a Task Event.', ) def InitOutgoingAttributes (self): """ Init any attributes on ourself that are appropriate for a new outgoing item. """ try: super(TaskEventExtraMixin, self).InitOutgoingAttributes () except AttributeError: pass TaskEventExtraMixin._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 the dueByDate to the task's dueDate try: self.dueByDate = self.dueDate except AttributeError: pass
class CollectionColors(schema.Item): """ Temporarily put the CollectionColors here until we refactor collection to remove display information """ colors = schema.Sequence(ColorType) colorIndex = schema.One(schema.Integer) def nextColor(self): color = self.colors[self.colorIndex] self.colorIndex += 1 if self.colorIndex == len(self.colors): self.colorIndex = 0 return color
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 NotifyHandler(schema.Item): """ An item that exists only to handle notifications we should change notifications to work on callables -- John is cool with that. """ log = schema.Sequence(initialValue=[]) collectionEventHandler = schema.One(schema.String, initialValue="onCollectionEvent") def checkLog(self, op, item, other): if len(self.log) == 0: return False rec = self.log[-1] return rec[0] == op and rec[1] == item and rec[2] == "rep" and rec[3] == other and rec[4] == () def onCollectionEvent(self, op, item, name, other, *args): self.log.append((op, item, name, other, args))
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 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 Tag(pim.ContentItem): """ Tag are items with bidirectional references to all the FlickrPhoto's with that tag. This makes it easy to find all photos with a given tag or all tags belonging to a photo. Currently, there isn't any code that takes advantage of Tags. """ itemsWithTag = schema.Sequence(FlickrPhotoMixin, inverse=FlickrPhotoMixin.tags) @classmethod def getTag(cls, view, tagName): """ Factory Method for getting a Tag. Lookup or create a Tag based on the supplied name string. If a matching Tag object is found in the repository, it is returned. If there is no match, then a new item is created and returned. @param tagName: name of the Tag @type tagName: C{String} @return: C{Tag} created or found """ # make sure the tagName looks reasonable assert tagName, "Invalid tagName passed to getTag factory" # get all Tag objects whose displayName match the param # return the first match found, if any for i in Tag.iterItems(view, exact=True): if i.displayName == tagName: return i # make a new Tag newTag = Tag(itsView=view) newTag.displayName = tagName return newTag
class ListCollection(AbstractCollection): """ """ schema.kindInfo( displayName="ListCollection" ) refCollection = schema.Sequence(otherName=Item.collections,initialValue=[]) def __init__(self, *args, **kw): super(ListCollection, self).__init__(*args, **kw) self.rep = Set((self,'refCollection')) self.refCollection = [] def add(self, item): self.refCollection.append(item) def remove(self, item): self.refCollection.remove(item) def contentsUpdated(self, item): self._collectionChanged('changed' , 'rep', item) pass
class StampCollection(ContentCollection): """ A C{ContentCollection} to store the collection of items that have a particular C{Stamp}. Used in favor of C{ListCollection} because there is code (in recurrence, for example) that manipulates C{ContentItem.collections}, the inverse of C{ListCollection.inclusions} in a way that breaks stamp collection membership. """ __metaclass__ = schema.CollectionClass __collection__ = 'stampedItems' schemaItem = schema.One(StampItem, inverse=StampItem._collection) stampedItems = schema.Sequence(Stamp, inverse=Stamp.stampCollections, initialValue=[]) @property def stamp_type(self): return self.schemaItem.stampClass def __repr__(self): return "<%s(%s): %s>" % (type(self).__name__, self.schemaItem.stampClass, self.itsUUID.str16())
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 Stamp(schema.Annotation): __metaclass__ = StampClass # should be Note? or even Item? Or leave it up to subclasses? schema.kindInfo(annotates=ContentItem) stampCollections = schema.Sequence(defaultValue=Empty) __use_collection__ = False @classmethod def getCollection(cls, repoView): if cls.__use_collection__: return schema.itemFor(cls, repoView).collection else: return None @classmethod def iterItems(cls, repoView): collection = cls.getCollection(repoView) if collection is not None: for item in collection: yield item @property def collection(self): # @@@ [grant] is this used anywhere return type(self).getCollection(self.itsItem.itsView) def get_stamp_types(self, allowInherit=True): stampCollections = self.stampCollections if (allowInherit and stampCollections is not Empty and stampCollections.isDeferred()): try: stampCollections = getattr(self.itsItem.inheritFrom, Stamp.stampCollections.name) except AttributeError: stampCollections = Empty return frozenset(coll.schemaItem.stampClass for coll in stampCollections) def set_stamp_types(self, values): old = self.get_stamp_types(allowInherit=False) for cls in old: if not cls in values: cls(self).remove() for cls in values: if not cls in old: cls(self).add() stamp_types = schema.Calculated(schema.Set, basedOn=stampCollections, fset=set_stamp_types, fget=get_stamp_types) @property def stamps(self): for t in self.stamp_types: yield t(self) def add(self): item = self.itsItem stampClass = self.__class__ stampCollection = schema.itemFor(stampClass, item.itsView).collection if stampCollection in self.stampCollections: raise StampAlreadyPresentError, \ "Item %r already has stamp %r" % (item, self) if (not self.itsItem.isProxy and not self.itsItem.hasLocalAttributeValue( Stamp.stampCollections.name)): self.stampCollections = [] self.stampCollections.add(stampCollection) if not item.isProxy: for attr, callback in stampClass.__all_ivs__: if not hasattr(self, attr): setattr(self, attr, callback(self)) for cls in stampClass.__mro__: # Initialize values for annotation attributes for attr, val in getattr(cls, '__initialValues__', ()): if not hasattr(self, attr): setattr(self, attr, val) def remove(self): item = self.itsItem stampClass = self.__class__ stampCollection = schema.itemFor(stampClass, item.itsView).collection if not stampCollection in self.stampCollections: raise StampNotPresentError, \ "Item %r doesn't have stamp %r" % (item, self) addBack = None if not item.isProxy: # This is gross, and was in the old stamping code. # Some items, like Mail messages, end up in the # all collection by virtue of their stamp. So, we # explicitly re-add the item to all after unstamping # if necessary. all = schema.ns("osaf.pim", item.itsView).allCollection if item in all: addBack = all if not self.itsItem.hasLocalAttributeValue( Stamp.stampCollections.name): self.stampCollections = list(self.stampCollections) self.stampCollections.remove(stampCollection) if addBack is not None and not item in addBack: addBack.add(item) def isAttributeModifiable(self, attribute): # A default implementation which sub-classes can override if necessary # ContentItem's isAttributeModifiable( ) calls this method on all of # an item's stamps return True @classmethod def addIndex(cls, view_or_collection, name, type, **keywds): try: addIndex = view_or_collection.addIndex except AttributeError: collection = cls.getCollection(view_or_collection) else: collection = view_or_collection return super(Stamp, cls).addIndex(collection, name, type, **keywds) @classmethod def update(cls, parcel, itsName, **attrs): targetType = cls.targetType() newAttrs = {} for key, value in attrs.iteritems(): if getattr(targetType, key, None) is None: key = getattr(cls, key).name newAttrs[key] = value item = targetType.update(parcel, itsName, **newAttrs) cls(item).add() return item def hasLocalAttributeValue(self, attrName): fullName = getattr(type(self), attrName).name return self.itsItem.hasLocalAttributeValue(fullName) @schema.observer(stampCollections) def onStampTypesChanged(self, op, attr): self.itsItem.updateDisplayDate(op, attr) self.itsItem.updateDisplayWho(op, attr)
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 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 Server(schema.Item): """ The web server Kind. Instances of this Kind are found via KindQuery at startup and activated. You may define a server item in your own parcel, and it will run as well as the default one defined in the webserver/servers parcel. """ port = schema.One(schema.Integer, displayName="Port", doc="The port to listen on") path = schema.One( schema.String, displayName="Path", doc="The filesystem path pointing to the server's doc root. This " "path is relative to the directory of the parcel.xml that " "defines the server item") resources = schema.Sequence( initialValue=(), displayName="Resources", doc="You may define custom twisted resources and associate them " "with this server") directories = schema.Sequence( initialValue=(), displayName="Directories", doc="You may specify other file system directories which will be " "used to server specific URL locations. (See the Directory " "Kind)") def startup(self): parcel = application.Parcel.Manager.getParentParcel(self) parcelDir = os.path.dirname(parcel.file) docRoot = os.path.join(parcelDir, self.path) root = static.File(docRoot) # .rpy files are twisted's version of a cgi root.ignoreExt(".rpy") root.processors = {".rpy": script.ResourceScript} logger.info("Activating web server on port %s with docroot %s" % \ (str(self.port), str(docRoot))) # Hook up all associated resources to a location under the docroot for res in self.resources: logger.info(" Hooking up /%s to resource '%s'" % \ (str(res.location), str(res.displayName))) resourceInstance = res.getResource() # Give the main thread repository view to the resource instance resourceInstance.repositoryView = self.itsView root.putChild(res.location, resourceInstance) # Hook up all associated directories to a location under the docroot for directory in self.directories: # First, find this directory's parcel, then determine that parcel's # directory, then join the directory.path. parcel = application.Parcel.Manager.getParentParcel(directory) parcelDir = os.path.dirname(parcel.file) docRoot = os.path.join(parcelDir, directory.path) logger.info(" Hooking up /%s to directory %s" % \ (str(directory.location), str(docRoot))) root.putChild(directory.location, static.File(docRoot)) site = server.Site(root) try: reactor.callFromThread(reactor.listenTCP, self.port, site) self.activated = True except twisted.internet.error.CannotListenError, e: logger.error("Twisted error: %s" % str(e)) print 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 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 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 RecurrenceRule(items.ContentItem): """One rule defining recurrence for an item.""" freq = schema.One(FrequencyEnum, displayName="Frequency possibilities", defaultValue="weekly") isCount = schema.One( schema.Boolean, displayName="isCount", doc="If True, calculate and export count instead of until", defaultValue=False) until = schema.One(schema.DateTime, displayName="Until", defaultValue=None) untilIsDate = schema.One( schema.Boolean, displayName="untilIsDate", doc="If True, treat until as an inclusive date, use until + 23:59 " "for until", defaultValue=True) interval = schema.One(schema.Integer, displayName="Interval", defaultValue=1) wkst = schema.One(WeekdayEnum, displayName="Week Start Day", defaultValue=None) bysetpos = schema.Sequence(schema.Integer, displayName="Position selector", defaultValue=None) bymonth = schema.Sequence(schema.Integer, displayName="Month selector", defaultValue=None) bymonthday = schema.Sequence(schema.Integer, displayName="Ordinal day of month selector", defaultValue=None) byyearday = schema.Sequence(schema.Integer, displayName="Ordinal day of year selector", defaultValue=None) byweekno = schema.Sequence(schema.Integer, displayName="Week number selector", defaultValue=None) byweekday = schema.Sequence(WeekdayAndPositionStruct, displayName="Weekday selector", defaultValue=None) byhour = schema.Sequence(schema.Integer, displayName="Hour selector", defaultValue=None) byminute = schema.Sequence(schema.Integer, displayName="Minute selector", defaultValue=None) bysecond = schema.Sequence(schema.Integer, displayName="Second selector", defaultValue=None) rruleFor = schema.One('RecurrenceRuleSet', inverse='rrules') exruleFor = schema.One('RecurrenceRuleSet', inverse='exrules') normalNames = "interval", "until" listNames = "bysetpos", "bymonth", "bymonthday", "byyearday", "byweekno",\ "byhour", "byminute", "bysecond" specialNames = "wkst", "byweekday", "freq" # dateutil automatically sets these from dtstart, we don't want these # unless their length is greater than 1. interpretedNames = "byhour", "byminute", "bysecond" def calculatedUntil(self): """Return until or until + 23:59, depending on untilIsDate.""" if self.untilIsDate: if self.until is None: return None else: return self.until.replace(hour=23, minute=59) else: return self.until def createDateUtilFromRule(self, dtstart): """Return an appropriate dateutil.rrule.rrule.""" kwargs = dict( (k, getattr(self, k)) for k in self.listNames + self.normalNames) for key in self.specialNames: if getattr(self, key) is not None: kwargs[key] = toDateUtil(getattr(self, key)) if self.until is not None and self.untilIsDate: kwargs['until'] = self.calculatedUntil() rule = rrule(dtstart=dtstart, **kwargs) if not self.isCount or self.until is None: 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.""" self.untilIsDate = False if rrule._count is not None: self.isCount = True # While most dates are naive, strip tzinfo off self.until = stripTZ(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 is not dateutil.rrule.WEEKLY or \ len(rrule._byweekday) != 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: listOfDayTuples.extend(tup for tup in rrule._bynweekday) if len(listOfDayTuples) > 0: self.byweekday = [] for day, n in listOfDayTuples: day = fromDateUtilWeekday(day) self.byweekday.append(WeekdayAndPositionStruct(day, n)) # While most dates are naive, strip tzinfo off if rrule._until is not None: self.until = stripTZ(rrule._until) if rrule._interval != 1: self.interval = rrule._interval for key in self.listNames: if getattr(rrule, '_' + key) is not None and \ (key not in self.interpretedNames or \ len(getattr(rrule, '_' + key)) > 1): # cast tuples to list, or will the repository do this for us? setattr(self, key, list(getattr(rrule, '_' + key))) # 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: if rrule._bymonthday[0] == rrule._dtstart.day: del self.bymonthday if rrule._freq == dateutil.rrule.YEARLY: if len(rrule._bymonth) == 1: if rrule._bymonth[0] == rrule._dtstart.month: del self.bymonth def onValueChanged(self, name): """If the rule changes, update any associated events.""" if name in self.listNames + self.normalNames + self.specialNames: for ruletype in ('rruleFor', 'exruleFor'): if self.hasLocalAttributeValue(ruletype): getattr(self, ruletype).onValueChanged('rrules')
class User(schema.Item, Principal): name = schema.One(schema.Text) accounts = schema.Sequence(otherName='user', initialValue=[])