示例#1
0
 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)
示例#2
0
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
示例#4
0
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'))
示例#6
0
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))