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)
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 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 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 CommunicationStatus(schema.Annotation): """ Generate a value that expresses the communications status of an item, such that the values can be compared for indexing for the communications status column of the Dashboard. The sort terms are: 1. unread, needs-reply, read 2. Not mail (and no error), sent mail, error, queued mail, draft mail 3a. If #2 is not mail: created, edited 3b. If #2 is mail: out, in, other 4b. If #2 is mail: firsttime, updated The constants used here map to the dashboard specification for Chandler: <http://svn.osafoundation.org/docs/trunk/docs/specs/rel0_7/Dashboard-0.7.html#comm-states> """ schema.kindInfo(annotates=ContentItem) # These flag bits govern the sort position of each communications state: # Terms with set "1"s further left will sort earlier. # 1: # (unread has neither of these set) # NEEDS_REPLY = 1 # READ = 1 # 2: # (non-mail has none of these set) # SENT = 1 # ERROR = 1 # QUEUED = 1 # DRAFT = 1 # 3a: # (created has this bit unset) # EDITED = 1 # 3b: # OUT = 1 # IN = 1 # NEITHER = 1 # (NEITHER is also called NEUTRAL in the spec). # 4b: # (firsttime has this bit unset) # UPDATE = 1 UPDATE, OUT, IN, NEITHER, EDITED, SENT, ERROR, QUEUED, DRAFT, NEEDS_REPLY, READ = ( 1 << n for n in xrange(11)) @staticmethod def getItemCommState(itemOrUUID, view=None): """ Given an item or a UUID, determine its communications state """ if view is None: view = itemOrUUID.itsView itemOrUUID = getattr(itemOrUUID, 'proxiedItem', itemOrUUID) modifiedFlags, lastMod, stampCollections, fromMe, \ toMe, needsReply, read, error, conflictingStates = \ view.findInheritedValues(itemOrUUID, *CommunicationStatus.attributeValues) stampTypes = set(collection.stamp_type for collection in stampCollections) result = 0 # error if error or conflictingStates: result |= CommunicationStatus.ERROR if MailStamp in stampTypes: # update: This means either: we have just # received an update, or it's ready to go # out as an update modification = Modification if (modification.updated == lastMod or (modification.sent != lastMod and modification.sent in modifiedFlags)): result |= CommunicationStatus.UPDATE # in, out, neither if toMe: result |= CommunicationStatus.IN if fromMe: result |= CommunicationStatus.OUT elif not toMe: result |= CommunicationStatus.NEITHER # queued if modification.queued in modifiedFlags: result |= CommunicationStatus.QUEUED # sent if lastMod in (modification.sent, modification.updated): result |= CommunicationStatus.SENT # draft if it's not one of sent/queued/error if result & (CommunicationStatus.SENT | CommunicationStatus.QUEUED | CommunicationStatus.ERROR) == 0: result |= CommunicationStatus.DRAFT # edited if Modification.edited in modifiedFlags: result |= CommunicationStatus.EDITED # needsReply if needsReply: result |= CommunicationStatus.NEEDS_REPLY # read if read: result |= CommunicationStatus.READ return result @staticmethod def dump(status): """ For debugging (and helpful unit-test messages), explain our flags. 'status' can be a set of flags, an item, or an item UUID. """ if not isinstance(status, int): status = CommunicationStatus.getItemCommState(status) if status == 0: return "(none)" result = [ flagName for flagName in ('UPDATE', 'OUT', 'IN', 'NEITHER', 'EDITED', 'SENT', 'QUEUED', 'DRAFT', 'NEEDS_REPLY', 'READ') if status & getattr(CommunicationStatus, flagName) ] return '+'.join(result) attributeValues = ( (ContentItem.modifiedFlags, frozenset()), (ContentItem.lastModification, None), (Stamp.stampCollections, frozenset()), (MailStamp.fromMe, False), (MailStamp.toMe, False), (ContentItem.needsReply, False), (ContentItem.read, False), (ContentItem.error, None), (SharedItem.conflictingStates, False), ) attributeDescriptors = tuple(t[0] for t in attributeValues) attributeValues = tuple((attr.name, val) for attr, val in attributeValues) status = schema.Calculated( schema.Integer, basedOn=attributeDescriptors, fget=lambda self: self.getItemCommState(self.itsItem), ) def addDisplayWhos(self, whos): """ Add tuples to the "whos" list: (priority, "text", source name) """ commState = CommunicationStatus.getItemCommState(self.itsItem) lastModifiedBy = self.itsItem.lastModifiedBy stamp_types = Stamp(self.itsItem).stamp_types isMessage = stamp_types and MailStamp in stamp_types if lastModifiedBy is not None: lastModifiedBy = lastModifiedBy.getLabel() # (bug 10927: We only want to include ed/up if the item is either not # a message, or not read) if (commState & (CommunicationStatus.EDITED | CommunicationStatus.UPDATE)) and \ (not isMessage or not (commState & CommunicationStatus.READ)): lastMod = self.itsItem.lastModification if lastMod == Modification.edited: whos.append((1, lastModifiedBy, 'editor')) else: whos.append((1, lastModifiedBy, 'updater')) else: whos.append((1000, lastModifiedBy, 'creator')) if isMessage: msg = MailStamp(self.itsItem) preferFrom = (commState & CommunicationStatus.OUT) == 0 toAddress = getattr(msg, 'toAddress', schema.Nil) toText = u", ".join(x.getLabel() for x in toAddress) whos.append((preferFrom and 4 or 2, toText, 'to')) originators = getattr(msg, 'originators', schema.Nil) if len(originators) > 0: originatorsText = u", ".join(x.getLabel() for x in originators) if len(originatorsText) > 0: whos.append((preferFrom and 2 or 3, originatorsText, 'from')) fromAddress = getattr(msg, 'fromAddress', None) if fromAddress is not None: fromText = fromAddress.getLabel() if len(fromText) > 0: whos.append((preferFrom and 3 or 4, fromText, 'from'))
class Certificate(pim.ContentItem): """ Certificate @see: U{model<../model/parcels/osaf/framework/certstore/Certificate/index.html>} """ purpose = schema.One( schema.Integer, doc='Certificate purpose.', initialValue=constants.PURPOSE_CA, ) trust = schema.One( schema.Integer, defaultValue=constants.TRUST_NONE, doc= 'A certificate can have no trust assigned to it, or any combination of 1=trust authenticity of certificate, 2=trust to issue server certificates.', ) pem = schema.One( schema.Lob, doc='An X.509 certificate in PEM format.', ) asText = schema.One( schema.Lob, doc='An X.509 certificate in human readable format.', ) fingerprintAlgorithm = schema.One( schema.Text, doc='A name of a hash algorithm that was used to compute fingerprint.', ) fingerprint = schema.One( schema.Text, doc= 'A hash of the certificate using algorithm named in fingerprintAlgorithm attribute.', ) def pemAsString(self): """ Get the pem attribute (which is stored as a LOB) as a str. @return: pem as str @rtype: str """ # M2Crypto needs this to be str rather than unicode - safe conversion return str(self.pem.getReader().read()) def getAsTextAsString(self): """ Get the asText attribute (which is stored as a LOB) as a str. @return: asText as unicode @rtype: unicode """ return self.asText.getPlainTextReader().read() asTextAsString = schema.Calculated(schema.Text, basedOn=(asText, ), fget=getAsTextAsString, doc="asText attribute as a string") def asX509(self): """ Get the pem (which is stored as a LOB) as a C{M2Crypto.X509.X509} instance. @return: pem as C{M2Crypto.X509.X509}. @rtype: C{M2Crypto.X509.X509} """ return X509.load_cert_string(self.pemAsString()) def isAttributeModifiable(self, attribute): # None of these attributes should be edited by the user. if attribute in [ 'date', 'purpose', 'fingerprintAlgorithm', 'fingerprint', 'asTextAsString' ]: return False return super(Certificate, self).isAttributeModifiable(attribute) @schema.observer(purpose, trust, pem) def changed(self, op, name): """ Get a change notification for an attribute change. This happens on item creation as well as normal attribute change (including deletion), but not on item deletion. """ # XXX Certificate should not need to know about ssl.certificateCache from osaf.framework.certstore import ssl ssl.certificateCache = [] def onItemDelete(self, view, isDeferring): """ Get a change notification for an item deletion. """ self.changed('remove', None)
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))