class BaseConduit(Conduit): sharePath = schema.One( schema.Text, initialValue=u"", doc="The parent 'directory' of the share", ) shareName = schema.One( schema.Text, initialValue=u"", doc="The 'directory' name of the share, relative to 'sharePath'", ) schema.initialValues(shareName=lambda self: unicode(UUID()), ) # TODO: see if this is used anymore: def isAttributeModifiable(self, item, attribute): share = self.share if utility.isSharedByMe(share) or share.mode in ('put', 'both'): return True # In old style shares, an attribute isn't modifiable if it's one # of the attributes shared for this item in this share for attr in item.getBasedAttributes(attribute): if attr in share.getSharedAttributes(item.itsKind): return False return True
class ClientIdentifier(pim.ContentItem): clientID = schema.One( schema.Bytes, doc='Used to identify different running instances of Chandler') schema.initialValues( clientID=lambda self: hashlib.sha256(self.itsUUID.str16()).hexdigest())
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 MailShare(Share): schema.initialValues( conduit=lambda self: MailConduit( itsParent=self, peerId=self.peerId, account=self.account), format=lambda self: CloudXMLDiffFormat(itsParent=self)) def sync(self, modeOverride=None, activity=None, forceUpdate=None): account = self.conduit.account account.login(None) return account.sync(self)
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 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 JabberAccount(Account): useSSL = schema.One(schema.Boolean, initialValue=False) password = schema.One(schema.Text) resource = schema.One(schema.Text, initialValue='chandler') schema.initialValues(protocol=lambda self: 'jabber') def isLoggedIn(self): client = self.client return client is not None and client.connected def getId(self): if '@' in self.userid: return "%s/%s" % (self.userid, self.resource) else: return "%s@%s/%s" % (self.userid, self.server, self.resource) def login(self, printf, autoLogin=False): client = self.client if client is None: repository = self.itsView.repository repoId, x, x = repository.getSchemaInfo() self.client = client = JabberClient(repoId, self, printf) worker = JabberWorker(repository) worker.start(client) reactor.callFromThread(client.connect, self.password, self.useSSL, False, self.itsView) def subscribe(self, peerId, name): if not self.isLoggedIn(): raise ValueError, "no connection" if '/' not in peerId: peerId += "/chandler" view = self.itsView sidebar = schema.ns('osaf.app', view).sidebarCollection share = None for collection in sidebar: if collection.displayName == name: for share in SharedItem(collection).shares: conduit = share.conduit if isinstance(conduit, JabberConduit): if conduit.peerId == peerId: return self.client.sync(share, None, None) return self.client.sync(None, peerId, name) def sync(self, share): if not self.isLoggedIn(): raise ValueError, "no connection" self.client.sync(share) return Nil
class JabberShare(Share): schema.initialValues( conduit=lambda self: JabberConduit( itsParent=self, peerId=peerId, account=self.account), format=lambda self: CloudXMLDiffFormat(itsParent=self))
class AppCollection(ContentCollection): """ AppCollections implement inclusions, exclusions, source, and trash along with methods for add and remove. """ __metaclass__ = schema.CollectionClass __collection__ = 'set' set = schema.One(schema.TypeReference('//Schema/Core/AbstractSet')) # must be named 'inclusions' to match ListCollection inclusions = inclusions # the exclusions used when no exclusions collection is given collectionExclusions = schema.Sequence(inverse=ContentItem.excludedBy, initialValue=[]) exclusionsCollection = schema.One(inverse=ContentCollection.exclusionsFor, defaultValue=None) trashCollection = schema.One(inverse=ContentCollection.trashFor, defaultValue=None) schema.initialValues( # better hope osaf.pim has been loaded! trash=lambda self: schema.ns('osaf.pim', self.itsView).trashCollection, source=lambda self: None, ) # __collection__ denotes a bi-ref set, # therefore it must be added to the copying cloud def for it to be copied. schema.addClouds(copying=schema.Cloud(byCloud=[exclusionsCollection], byRef=[ trashCollection, __collection__, inclusions, collectionExclusions ]), ) # an AppCollection may have another collection for exclusions and that # other collection may be the global trash collection. If no collection # is specified for exclusions, a local ref collection is used instead. def _getExclusions(self): exclusions = self.exclusionsCollection if exclusions is None: exclusions = self.collectionExclusions return exclusions def _setExclusions(self, exclusions): # Typically we will create a collectionExclusions ref collection; # however, a collection like 'All' will instead want to use the # Trash collection for exclusions if exclusions is None: self.collectionExclusions = [] self.exclusionsCollection = None else: self.exclusionsCollection = exclusions if hasattr(self, 'collectionExclusions'): del self.collectionExclusions self._updateCollection() exclusions = property(_getExclusions, _setExclusions) # an AppCollection may have another collection for trash. If no # collection is given for trash, the collection's exclusions is used # instead following the logic above. def _getTrash(self): trash = self.trashCollection if trash is None: trash = self.exclusions return trash def _setTrash(self, trash): """ In general trash should only be the well known Trash collection or None. None indicates that this collection does not participate in Trash-based activities. The special value of Default for trash is only a sentinel to let us know that nothing has been passed in and that the default trash should be looked up in osaf.pim. During parcel loading, this allows us to pass the trash into the constructor and avoid trying to look it up in osaf.pim while osaf.pim is being loaded. """ # You can designate a certain ListCollection to be used for this # collection's trash; in this case, an additional DifferenceCollection # will be created to remove any trash items from this collection. Any # collections which share a trash get the following benefits: # - Adding an item to the trash will make the item disappear from # collections sharing that trash collection # - When an item is removed from a collection, it will automatically # be moved to the trash if it doesn't appear in any collection which # shares that trash self.trashCollection = trash self._updateCollection() trash = property(_getTrash, _setTrash) def _getSource(self): return self.__source def _setSource(self, source): if hasattr(self, 'source'): self.__source = source self._updateCollection() else: self.__source = source source = property(_getSource, _setSource) def add(self, item): """ Add an item to the collection. """ self.inclusions.add(item) exclusions = self.exclusions if item in exclusions: exclusions.remove(item) # If a trash is associated with this collection, remove the item # from the trash. This has the additional benefit of having the item # reappear in any collection which has the item in its inclusions trash = self.trash if trash is not None and item in trash: trash.remove(item) def remove(self, item): """ Remove an item from the collection. """ isDeleting = item.isDeleting() # never add occurrences to the trash or exclusions lists, bug 10777 isOccurrence = item.itsRefs.get('inheritFrom') is not None # adding to exclusions before determining if the item should be added to # the trash was a problem at one point (bug 4551), but since the mine/ # not-mine mechanism changed, this doesn't seem to be a problem anymore, # and removing from a mine collection was actually misbehaving if the # test was done first, so now logic for moving to the trash has moved # back to after addition to exclusions and removal from inclusions. if not isDeleting and not isOccurrence: self.exclusions.add(item) if item in self.inclusions: self.inclusions.remove(item) trash = self.trash pim_ns = schema.ns('osaf.pim', self.itsView) if not (isDeleting or trash is None or isOccurrence): if isinstance(trash, ContentCollection): for collection in itertools.chain(trash.trashFor, [pim_ns.allCollection]): # allCollection isn't in trash.trashFor, but needs to be # considered if collection is not self and item in collection: # it exists somewhere else, definitely don't add # to trash break else: # we couldn't find it anywhere else, so it goes in the trash trash.add(item) def __setup__(self): # Ensure that the collection is properly set up self._updateCollection() def _updateCollection(self): if not hasattr(self, 'source'): # Can't initialize the collection until the source is known return exclusions = self.exclusionsCollection if exclusions is None: exclusions = (self, 'collectionExclusions') innerSource = (self, 'inclusions') if self.source is not None: innerSource = Union(self.source, innerSource) set = Difference(innerSource, exclusions) trash = self.trashCollection if trash is not None: set = Difference(set, trash) setattr(self, self.__collection__, set) def withoutTrash(self, copy=True): """ Pull out the non-trash part of AppCollection. Smart collections are 'special' - they almost always include the trash as a part of their structure on the _right side of their Difference set. This means that when they are hooked into a larger collection tree, they need to only give out the _left side, which has no trash. """ if self.trash is schema.ns('osaf.pim', self.itsView).trashCollection: left = getattr(self, self.__collection__)._left if copy: return left.copy(self.itsUUID) return left return self
class MethodIndexDefinition(IndexDefinition): """ A class that allows you to build indexes based on comparing computed attributes (i.e. ones that aren't stored in the repository directly). Note that this class reinterprets the 'attributes' attribute of IndexDefinition: this is now the attributes to _monitor_ (i.e. the ones that trigger recomputing the index). @cvar findValuePairs: The pairs you want instances to pass to C{IndexDefinition.findValues()} when the index is asked to compare two UUIDs. @type findValuePairs: C{tuple} """ findValuePairs = () def makeIndexOn(self, collection, kind=None): """ Create the index we describe on this collection """ monitoredAttributes = (self.attributes or []) # We need to include inheritFrom in the attributes we monitor, # else (especially at Occurrence creation time) items don't get # re-indexed properly when they inherit attribute values from # their "rich relatives" (ovaltofu's term). if not 'inheritFrom' in monitoredAttributes: monitoredAttributes = ( 'inheritFrom', ) + tuple(monitoredAttributes) collection.addIndex(self.itsName, 'method', method=(self, 'compare'), monitor=monitoredAttributes, kind=kind) def compare(self, index, u1, u2, vals): """ Compare two items, given their UUIDs. This method fetches (using findValues() on the item UUIDs) the pairs specified in the C{findValuePairs} (class) variable. """ if not self.findValuePairs: raise TypeError( "pim.MethodIndexDefinition is an abstract type; use a " \ "subtype that sets findValuePairs" ) view = self.itsView if u1 in vals: v1 = vals[u1] else: v1 = view.findInheritedValues(u1, *self.findValuePairs) if u2 in vals: v2 = vals[u2] else: v2 = view.findInheritedValues(u1, *self.findValuePairs) return cmp(v1, v2) def compare_init(self, index, u, vals): return self.itsView.findInheritedValues(u, *self.findValuePairs) schema.initialValues( # Make the attributes we monitor be the same as the ones we'll # fetch in findValues(). attributes=lambda self: [tuple[0] for tuple in type(self).findValuePairs])
class Note(items.ContentItem): icalUID = schema.One( schema.Text, doc="iCalendar uses arbitrary strings for UIDs, not UUIDs. We can " "set UID to a string representation of UUID, but we need to be " "able to import iCalendar events with arbitrary UIDs.") icalendarExtra = schema.One( schema.Text, defaultValue='', doc="Fragment of an icalendar file containing unrecognized data " "in iCalendar format.") # icalendarProperties and icalendarParameters are preserved just until # the old XML + ics dual fork code goes away, the attribute that's used # by current code is icalendarExtra icalendarProperties = schema.Mapping( schema.Text, defaultValue=Empty, doc="Original icalendar property name/value pairs not understood " "by Chandler. Subcomponents (notably VALARMS) aren't stored.") icalendarParameters = schema.Mapping( schema.Text, defaultValue=Empty, doc="property name/parameter pairs for parameters not understood by " "Chandler. The parameter value is the concatenation of " "paramater key/value pairs, separated by semi-colons, like the " "iCalendar serialization of those parameters") schema.addClouds(sharing=schema.Cloud(literal=[icalUID], )) schema.initialValues(icalUID=lambda self: unicode(self.itsUUID)) def InitOutgoingAttributes(self): """ Init any attributes on ourself that are appropriate for a new outgoing item. """ super(Note, self).InitOutgoingAttributes() self.processingStatus = 'processing' 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 super(Note, self).ExportItemData(clipboardHandler) # Let the clipboard handler know we've got a Note to export clipboardHandler.ExportItemFormat(self, 'Note') def addDisplayDates(self, dates, now): super(Note, self).addDisplayDates(dates, now) for stampObject in Stamp(self).stamps: method = getattr(stampObject, 'addDisplayDates', lambda _, __: None) method(dates, now) def addDisplayWhos(self, whos): super(Note, self).addDisplayWhos(whos) for stampObject in Stamp(self).stamps: method = getattr(stampObject, 'addDisplayWhos', lambda _: None) method(whos) from osaf.communicationstatus import CommunicationStatus CommunicationStatus(self).addDisplayWhos(whos)
class Triageable(Remindable): """ An item with triage status, in all its flavors""" # Notes: # # - There are two sets of triage status attributes: # - 'triageStatus' is used to set (and is changed by) the dashboard # column cell and the detail view markup-bar button. It's also used # for ordering in the dashboard unless there's this: # - 'sectionTriageStatus' overrides triageStatus for sorting, if it # exists; the 'Triage' toolbar button removes the sectionTriageStatus # attributes to force re-sorting by triageStatus alone. # # - Each actually consists of two attributes: the actual status attribute, # and a timestamp (in UTC) that says how recently the attribute was # changed; it's used for subsorting in each triage status section in the # dashboard. # # - These attributes are generally set only by the setTriageStatus method, # and read using the Calculated properties. # which takes care of updating the timestamp attributes appropriately. # The raw attributes are directly accessible if you want to avoid the # magic (attn: sharing and recurrence :-) ) _triageStatus = schema.One(TriageEnum, defaultValue=TriageEnum.now, indexed=True) _sectionTriageStatus = schema.One(TriageEnum) _triageStatusChanged = schema.One(schema.Float, defaultValue=None) _sectionTriageStatusChanged = schema.One(schema.Float) # Should we autotriage when the user changes a date or when an alarm fires? # Normally yes, so starts True; no once the user has manually set triage # status. # @@@ currently, never reset to yes; possible a 1.0 task will do this when # triage is set (either manually, or by the user) to the value it would have # if autotriaged... or something like that. doAutoTriageOnDateChange = schema.One(schema.Boolean, defaultValue=True) schema.addClouds( sharing=schema.Cloud( # To avoid compatibility issues with old shares, I didn't add # doAutoTriageOnDateChange to the sharing cloud -- this is all # going away soon anyway... literal=[_triageStatus, _triageStatusChanged], ), ) schema.initialValues(_triageStatusChanged=lambda self: self. makeTriageStatusChangedTime(self.itsView)) @staticmethod def makeTriageStatusChangedTime(view, when=None): # get a float representation of a time from 'when' (or the current # time if when is None or not passed) if isinstance(when, float): pass # nothing to do elif isinstance(when, datetime): # (mktime wants local time, so make sure 'when' is # in the local timezone) when = -time.mktime( when.astimezone(view.tzinfo.default).timetuple()) else: when = -time.time() return when def setTriageStatus(self, newStatus=None, when=None, pin=False, popToNow=False, force=False): """ Set triageStatus to this value, and triageStatusChanged to 'when' if specified (or the current time if not). Newstatus can be 'auto' to autotriage this item based on its inherent times (eg, startTime for an event, or alarm time if the item has one). If pin, save the existing triageStatus/triageStatusChanged in _sectionTriageStatus & _sectionTriageStatusChanged, which will have the effect of keeping the item in place in the dashboard until the next purge. (If there's already a _sectionTriageStatus value, don't overwrite it, unless force is True.) If popToNow, use _sectionTriageStatus/_sectionTriageStatusChanged to pop the item to the top of the Now section (again, only if force if the item already has section status). """ if (hasattr(self, 'proxiedItem') and getattr(self.proxiedItem, 'inheritFrom', None) and (not self.changing or self.changing.__name__ != 'CHANGE_THIS')): # setTriageStatus on a proxy should do nothing, unless it's # changing a single occurrence return # Don't autotriage unless the flag says we should. (We keep going if we # also want to pop-to-now, though) if newStatus == 'auto' and not popToNow \ and not self.doAutoTriageOnDateChange: #from osaf.framework.blocks.Block import debugName #logger.debug("Not Autotriaging %s", debugName(self)) return # Don't reindex or notify until we're done with these changes view = self.itsView with view.observersDeferred(): with view.reindexingDeferred(): # Manipulate section status if necessary if pin: if not hasattr(self, '_sectionTriageStatus'): self.__setTriageAttributes(self._triageStatus, self._triageStatusChanged, True, force) elif popToNow: #from osaf.framework.blocks.Block import debugName #logger.debug("Popping %s to Now", debugName(self)) self.__setTriageAttributes(TriageEnum.now, None, True, force) else: # We're not pinning in place, and we're not popping to Now. # Get rid of any existing section triage status if self.hasLocalAttributeValue('_sectionTriageStatus'): del self._sectionTriageStatus if self.hasLocalAttributeValue( '_sectionTriageStatusChanged'): del self._sectionTriageStatusChanged # Autotriage if we're supposed to. if newStatus == 'auto': # Skip out if we're not supposed to. if not self.doAutoTriageOnDateChange: return # Give our stamps a chance to autotriage newStatus = None from stamping import Stamp for stampObject in Stamp(self).stamps: # If the stamp object has an autoTriage method, and # returns True when we call it, we're done. method = getattr(type(stampObject), 'autoTriage', None) if method is not None: newStatus = method(stampObject) if newStatus is not None: if isinstance(newStatus, tuple): # The stamp specified a time too - note it. (newStatus, when) = newStatus else: when = None break if newStatus is None: # The stamps didn't do it; put ourself in Later if we # have a future reminder. Otherwise, leave things be. reminder = self.getUserReminder() if reminder is not None \ and reminder.nextPoll is not None \ and reminder.nextPoll != reminder.farFuture \ and reminder.nextPoll > datetime.now(view.tzinfo.default): #from osaf.framework.blocks.Block import debugName #logger.debug("Autotriaging %s to LATER for reminder", #debugName(self)) newStatus = TriageEnum.later # If we were given, or calculated, a triage status, set it. if newStatus is not None: self.__setTriageAttributes(newStatus, when, False, True) triageStatus = schema.Calculated(TriageEnum, fget=lambda self: self._triageStatus, basedOn=(_triageStatus, )) triageStatusChanged = schema.Calculated( schema.Float, fget=lambda self: self._triageStatusChanged, basedOn=(_triageStatusChanged, )) sectionTriageStatus = schema.Calculated( TriageEnum, fget=lambda self: getattr(self, '_sectionTriageStatus', self. _triageStatus), basedOn=(_sectionTriageStatus, _triageStatus), doc="Allow _sectionTriageStatus to override triageStatus") def __setTriageAttributes(self, newStatus, when, section, force): """ Common code for setTriageStatus and setSectionTriageStatus """ # Don't if we already have this attribute pair, unless we're forcing. tsAttr = '_sectionTriageStatus' if section else '_triageStatus' if not force and hasattr(self, tsAttr): return # Don't if we're in the middle of sharing... # @@@ I'm not sure if this is still necessary... if getattr(self, '_share_importing', False): return tscValue = Triageable.makeTriageStatusChangedTime(self.itsView, when) setattr(self, tsAttr, newStatus) tscAttr = '_sectionTriageStatusChanged' if section else '_triageStatusChanged' setattr(self, tscAttr, tscValue) def copyTriageStatusFrom(self, item): self._triageStatus = item._triageStatus if item._triageStatusChanged is not None: self._triageStatusChanged = item._triageStatusChanged if hasattr(item, '_sectionTriageStatus'): self._sectionTriageStatus = item._sectionTriageStatus stsc = getattr(item, '_sectionTriageStatusChanged', None) if stsc is not None: self._sectionTriageStatusChanged = stsc elif hasattr(self, '_sectionTriageStatus'): del self._sectionTriageStatus if hasattr(self, '_sectionTriageStatusChanged'): del self._sectionTriageStatusChanged self.doAutoTriageOnDateChange = item.doAutoTriageOnDateChange def purgeSectionTriageStatus(self): """ If this item has section status that's overriding its triage status, purge it. """ for attr in ('_sectionTriageStatus', '_sectionTriageStatusChanged'): inheritedFrom = getattr(self, 'inheritFrom', None) if inheritedFrom is not None and hasattr(inheritedFrom, attr): delattr(inheritedFrom, attr) if hasattr(self, attr): delattr(self, attr) def resetAutoTriageOnDateChange(self): """ The user changed triage status. Disable certain future automatic triaging @@@ Future: ... unless this change is to the status that the item would be triaged to if we autotriaged it now, in which case we re-enable future autotriaging. """ self.doAutoTriageOnDateChange = False def reminderFired(self, reminder, when): """ Override of C{Remindable.reminderFired}: sets triageStatus to now as of the time the reminder was due. """ pending = super(Triageable, self).reminderFired(reminder, when) self.setTriageStatus(TriageEnum.now, when=when) self.resetAutoTriageOnDateChange() return pending if __debug__: def triageState(self): """ For debugging, collect the triage status variables in a tuple """ def changedToDate(c): return None if c is None else time.asctime(time.gmtime(-c)) return (getattr(self, '_triageStatus', None), changedToDate(getattr(self, '_triageStatusChanged', None)), getattr(self, '_sectionTriageStatus', None), changedToDate( getattr(self, '_sectionTriageStatusChanged', None)), getattr(self, 'doAutoTriageOnDateChange', None))
class Script(pim.ContentItem): """ Persistent Script Item, to be executed. """ lastRan = schema.One(schema.DateTime) fkey = schema.One(schema.Text, initialValue=u'') test = schema.One(schema.Boolean, initialValue=False) filePath = schema.One(schema.Text, initialValue=u'') lastSync = schema.One(schema.DateTime) schema.initialValues( private=lambda self: False, # can share scripts displayName=lambda self: # XXX check if itsName is a UUID? unicode(self.itsName) if self.itsName else messages.UNTITLED, ) def __setup__(self): self.lastRan = self.lastSync = datetime.now() def execute(self): self.sync_file_with_model() self.lastRan = datetime.now() # this is a nasty hack to import from proxy.py. # This is the only way that scripts know about app_ns # and other chandler-specific proxies from proxy import app_ns run_script_with_symbols(self.body, fileName=self.filePath, builtIns=dict(app_ns=app_ns)) def set_body_quietly(self, newValue): if newValue != self.body: oldQuiet = getattr(self, '_change_quietly', False) self._change_quietly = True self.body = newValue self._change_quietly = oldQuiet @schema.observer(pim.ContentItem.body) def onBodyChanged(self, op, name): self.model_data_changed() def model_data_changed(self): if self.filePath and not getattr(self, '_change_quietly', False): self.modelModTime = datetime.now() def sync_file_with_model(self, preferFile=False): """ Synchronize file and model - which ever is latest wins, with conflict dialogs possible if both have changed since the last sync. """ if self.filePath and not getattr(self, '_change_quietly', False): writeFile = False # make sure we have a model modification time if not hasattr(self, 'modelModTime'): self.modelModTime = self.lastSync # get modification date for the file try: fileModTime = datetime.fromtimestamp(os.stat(self.filePath)[8]) except OSError: fileModTime = self.lastSync writeFile = True if preferFile or fileModTime > self.modelModTime: self.set_body_quietly(self.file_contents(self.filePath)) elif writeFile or fileModTime < self.modelModTime: # model is newer if fileModTime > self.lastSync: msg = _( u"The file associated with this script has changes that are older than your recent edits.\n\nDo you want to overwrite the older changes?" ) caption = _(u"Overwrite Script File") if wx.MessageBox(msg, caption, style=wx.YES_NO, parent=wx.GetApp().mainFrame) == wx.NO: return self.write_file(self.body) fileModTime = datetime.fromtimestamp(os.stat(self.filePath)[8]) # now the file and model match self.lastSync = self.modelModTime = fileModTime def file_contents(self, filePath): """ return the contents of our script file """ scriptFile = open(filePath, 'rt') try: scriptText = scriptFile.read(-1) finally: scriptFile.close() return scriptText def write_file(self, scriptText): """ write the contents of our script file into the file """ scriptFile = open(self.filePath, 'wt') try: scriptText = scriptFile.write(scriptText) finally: scriptFile.close() def set_file(self, fileName, siblingPath): #Convert fileName to utf8 encoding #to bytes to prevent the join function from trying to downcast #the unicode fileName to ascii if isinstance(fileName, unicode): fileName = fileName.encode('utf8') #Convert the filePath bytes to unicode for storage filePath = unicode(os.path.join(os.path.dirname(siblingPath), \ fileName), sys.getfilesystemencoding()) self.body = self.file_contents(filePath) self.filePath = filePath
class TimeZoneInfo(schema.Item): """ Item that persists: - A schema.TimeZone attribute that synchronizes itself with PyICU's default settings. - A list of "well-known" timezone names. """ default = schema.One(schema.TimeZone) # List of well-known time zones (for populating drop-downs). # [i18n] Since ICU doesn't suitably localize strings like 'US/Pacific', # we'll have to provide our own translations. wellKnownIDs = schema.Sequence(schema.Text, ) schema.initialValues(default=lambda self: self.itsView.tzinfo.floating) # Observe changes to 'default'. # When the view's default timezone changes via another route such as # refresh(), ontzchange is invoked by the repository @schema.observer(default) def onDefaultChanged(self, op, name): # Make sure that the view's default timezone is synched with ours view = self.itsView default = self.default # only set the view's default timezone if timezones are used if default is not None and default != view.tzinfo.floating: assert view.tzinfo.ontzchange is ontzchange view.tzinfo.setDefault(default) # --> ontzchange else: self.default = self.canonicalTimeZone(default) @classmethod def get(cls, view): """ Return the default C{TimeZoneInfo} instance, which automatically syncs with the view's default; i.e. if you assign an ICUtzinfo to C{TimeZoneInfo.get().default}, this will be stored as the view's default time zone. """ return schema.ns(__name__, view).defaultInfo def canonicalTimeZone(self, tzinfo): """ This returns an ICUtzinfo that's equivalent to the passed-in tzinfo, to prevent duplicates (like 'PST' and 'US/Pacific' from appearing in timezone pickers). A side-effect is that if a previously unseen tzinfo is passed in, it will be added to the receiver's wellKnownIDs. """ view = self.itsView if tzinfo is None or tzinfo == view.tzinfo.floating: result = view.tzinfo.floating else: result = None if tzinfo.tzid in self.wellKnownIDs: result = tzinfo else: for equivName in equivalentTZIDs(tzinfo): if equivName in self.wellKnownIDs: result = view.tzinfo.getInstance(equivName) break if result is None and tzinfo is not None and tzinfo.tzid in olson_tzids: self.wellKnownIDs.append(unicode(tzinfo.tzid)) result = tzinfo return result def iterTimeZones(self, withFloating=True): """ A generator for all the well-known ICUtzinfo objects. Each generated value is a tuple of the form (display name, ICUtzinfo), where 'display name' is a suitably localized unicode string. """ view = self.itsView floating = view.tzinfo.floating for name in self.wellKnownIDs: tzinfo = view.tzinfo.getInstance(name) if tzinfo != floating: yield (ChandlerMessageFactory(name), tzinfo) if withFloating: # L10N: Entry in the 'timezone' drop-down menu in the detail view # L10N: when an event has no time zone (also known as "Floating" # L10N: time). In English, this is translated as "None", but I # L10N: didn't want to use "None" in a msgid because that could # L10N: be used in other context (e.g. "alarm" dropdown). _ = ChandlerMessageFactory # we want this translated yield _(u"None (timezone)"), floating
class MailAccount(Account): smtp = schema.One(schema.Item, inverse=schema.Sequence()) imap = schema.One(schema.Item, inverse=schema.Sequence()) schema.initialValues( protocol=lambda self: 'mail', userid=lambda self: self.imap.username, server=lambda self: self.imap.host, ) def isLoggedIn(self): return self.client is not None def login(self, printf, autoLogin=False): client = self.client if client is None: repository = self.itsView.repository repoId, x, x = repository.getSchemaInfo() self.client = client = MailClient(repoId, self, printf) worker = MailWorker(repository) worker.start(client) def send(self, peerId, name): if not self.isLoggedIn(): raise ValueError, "no mail client" view = self.itsView sidebar = schema.ns('osaf.app', view).sidebarCollection for collection in sidebar: if collection.displayName == name: for share in SharedItem(collection).shares: conduit = share.conduit if isinstance(conduit, MailConduit): if conduit.peerId == peerId: return self.client.send(share, None, None, 'sync') return self.client.send(None, peerId, name, 'send') def check(self, peerId, name): if not self.isLoggedIn(): raise ValueError, "no mail client" self.client.check(None, peerId, name, 'sync') def sync(self, share): if not self.isLoggedIn(): raise ValueError, "no mail client" if share.ackPending: self.client.check(share, None, None, 'receipt') self.client.check(share, None, None, 'sync') self.client.send(share, None, None, 'sync') return Nil
class FeedItem(pim.Note): """ This class implements a feed channel item that is visualized in the summary and detail views. """ # # FeedItem repository interface # link = schema.One(schema.URL, initialValue=None) category = schema.One(schema.Text, indexed=True) author = schema.One(schema.Text, indexed=True) date = schema.One(schema.DateTime) channel = schema.One(FeedChannel) content = schema.One(schema.Lob, indexed=True) updated = schema.One(schema.Boolean) @apply def body(): def fget(self): return self.content def fset(self, value): self.content = value return property(fget, fset) schema.addClouds( sharing = schema.Cloud( literal = [link, category, author, date] ) ) schema.initialValues( displayName = lambda self: _(u"No Title") ) def _compareLink(self, other): """ This method compares two feed items. """ return cmp(str(self.link).lower(), str(other.link).lower()) def refresh(self, data): """ This method updates a feed item content. """ # fill in the item attrs = {"title":"displayName"} setAttributes(self, data, attrs) attrs = ["link", "category", "author"] # @@@MOR attrs = ["creator", "link", "category"] setAttributes(self, data, attrs) content = data.get("content") # Use the "content" info first, falling back to what"s in "description" if content: content = content[0]["value"] else: content = data.get("description") if content: self.content = self.getAttributeAspect("content", "type").makeValue(content, indexed=True) if "date" in data: self.date = date_parse(self.itsView, str(data.date)) else: # No date was available in the feed, so assign it "now" self.date = datetime.now(self.itsView.tzinfo.default) @schema.observer(author) def onAuthorChange(self, op, attr): self.updateDisplayWho(op, attr) def addDisplayWhos(self, whos): super(FeedItem, self).addDisplayWhos(whos) author = getattr(self, 'author', None) if author is not None: whos.append((10, author, 'author'))