Пример #1
0
class Startup(schema.Item):
    """Subclass this & create parcel.xml instances for startup notifications"""

    invoke = schema.One(
        schema.String,
        doc="Full name of a class or function to import and run at startup")

    active = schema.One(
        schema.Boolean,
        doc="Set to False to disable invocation of this item at startup",
        initialValue=True,
    )

    requires = schema.Sequence("Startup",
                               doc="Startups that must run before this one",
                               initialValue=[])

    requiredBy = schema.Sequence(inverse=requires)

    def getTarget(self):
        """Import the object named by ``invoke`` and return it"""
        return schema.importString(self.invoke)

    def invokeTarget(self, target):
        """Run the specified startup target in the current thread"""
        target(self)

    def onStart(self):
        """Override this method in a subclass to receive the notification

        Note: you should *not* create or modify items in this method or code
        called from this method.  If you want to do that, you probably don't
        want to be using a Startup item.

        Also note that you should not call invoke this method via super()
        unless you want the default behavior (i.e., importing and running the
        ``invoke`` attribute) to occur.
        """
        self.invokeTarget(self.getTarget())

    def _start(self, attempted, started):
        """Handle inter-startup ordering and invoke onStart()"""
        if self in started:
            return True
        elif not self.active or self in attempted:
            return False

        attempted.add(self)  # prevent multiple attempts to start this item
        canStart = True
        for item in self.requires:
            canStart = canStart and item._start(attempted, started)

        if canStart:
            self.onStart()
            started.add(self)
            return True

        return False
Пример #2
0
class BranchSubtree(schema.Annotation):
    """
    A mapping between an Item and the list of top-level blocks ('rootBlocks')
    that should appear when an Item inheriting from that Kind is displayed.
    Each rootBlock entry should have its 'position' attribute specified, to
    enable it to be sorted with other root blocks.)
    """
    schema.kindInfo(annotates=schema.Kind)
    rootBlocks = schema.Sequence(Block, inverse=schema.Sequence())
Пример #3
0
class ColumnHeader(RectangularChild):
    """This class defines a generic ColumnHeader kind."""

    # -- Attributes for ColumnHeader -- #

    columnHeadings = schema.Sequence(schema.Text, required=True)
    columnWidths = schema.Sequence(schema.Integer)
    proportionalResizing = schema.One(schema.Boolean)
    visibleSelection = schema.One(schema.Boolean)
    genericRenderer = schema.One(schema.Boolean)

    # add selection attribute?

    def ResizeHeader(self):
        for (i, width) in enumerate(self.columnWidths):
            if hasattr(self, "widget"):
                self.widget.SetItemSize(i, (width, 0))

    def defaultOnSize(self, event):
        self.ResizeHeader()
        event.Skip()

    def instantiateWidget(self):

        # create widget instance as a child of the parent block's widget.
        wxColHeaderInstance = colheader.ColumnHeader(self.parentBlock.widget)

        # FYI: currently, calendar needs proportional resizing off (false), because sizing needs to be exact
        wxColHeaderInstance.SetAttribute(
            colheader.CH_ATTR_ProportionalResizing, self.proportionalResizing)

        # set attributes
        if hasattr(self, "visibleSelection"):
            wxColHeaderInstance.SetAttribute(
                colheader.CH_ATTR_VisibleSelection, self.visibleSelection)
        if hasattr(self, "proportionalResizing "):
            wxColHeaderInstance.SetAttribute(
                colheader.CH_ATTR_ProportionalResizing,
                self.proportionalResizing)
        if hasattr(self, "genericRenderer"):
            wxColHeaderInstance.SetAttribute(colheader.CH_ATTR_GenericRenderer,
                                             self.genericRenderer)

        # add columns.
        for header in self.columnHeadings:
            wxColHeaderInstance.AppendItem(header,
                                           wx.ALIGN_CENTER,
                                           20,
                                           bSortEnabled=False)

        # set a default size-event handler  (this may need to be removed)
        wxColHeaderInstance.Bind(wx.EVT_SIZE, self.defaultOnSize)

        wxColHeaderInstance.Layout()

        return wxColHeaderInstance
class FilteredCollection(SingleSourceWrapperCollection):
    """
    A ContentCollection which is the result of applying a boolean predicate
    to every item of another ContentCollection.

    The C{source} attribute contains the ContentCollection instance to be
    filtered.

    The C{filterExpression} attribute is a string containing a Python
    expression. If the expression returns C{True} for an item in the
    C{source} it will be in the FilteredCollection.

    The C{filterAttributes} attribute is a list of attribute names
    (Strings), which are accessed by the C{filterExpression}.
    Failure to provide this list will result in missing notifications.
    """

    filterExpression = schema.One(schema.Text)
    filterMethod = schema.One(schema.Tuple)
    filterAttributes = schema.Sequence(schema.Importable, initialValue=[])

    def _sourcesChanged_(self, op):

        source = self.source
        if source is None:
            s = EmptySet()
        else:
            attrs = tuple(set(self.filterAttributes))
            if hasattr(self, 'filterExpression'):
                s = ExpressionFilteredSet(source, self.filterExpression, attrs)
            else:
                s = MethodFilteredSet(source, self.filterMethod, attrs)

        setattr(self, self.__collection__, s)
        return s
Пример #5
0
class Project(ContentItem):

    schema.kindInfo(
        displayName="Project",
        examples=[
            'my "Housewarming Party" project',
            "my department's \"Move to new building\" project",
            "my company's \"Open Seattle Store\" project",
        ],
        description=
        "Users can create projects to help organize their work. Users can "
        "take content items (like tasks and mail messages) and assign "
        "them to different projects.")

    parentProject = schema.One(
        'Project',
        displayName='Parent Project',
        doc=
        'Projects can be organized into hierarchies. Each project can have one parent.',
        inverse='subProjects',
    )
    subProjects = schema.Sequence(
        'Project',
        displayName='Sub Projects',
        doc=
        'Projects can be organized into hierarchies. Each project can have many sub-projects.',
        inverse='parentProject',
    )
Пример #6
0
class ModifyContentsEvent(BlockEvent):
    items = schema.Sequence(schema.Item, initialValue=[])
    operation = schema.One(operationType, initialValue='add')
    copyItems = schema.One(schema.Boolean, initialValue=True)
    selectFirstItem = schema.One(schema.Boolean, initialValue=False)
    disambiguateItemNames = schema.One(schema.Boolean, initialValue=False)
    schema.addClouds(copying=schema.Cloud(byRef=[items]))
Пример #7
0
class Account(schema.Item):

    userid = schema.One(schema.Text)
    server = schema.One(schema.Importable)
    protocol = schema.One(schema.Symbol)
    user = schema.One(otherName='accounts', initialValue=None)
    default = schema.One(schema.Boolean, initialValue=False)
    autoLogin = schema.One(schema.Boolean, initialValue=False)
    conduits = schema.Sequence(otherName='account')

    _clients = {}

    schema.initialValues(
        user=lambda self: User(itsView=self.itsView, name=self.userid))

    def login(self, printf, autoLogin=False):
        raise NotImplementedError, "%s.login" % (type(self))

    def isLoggedIn(self):
        raise NotImplementedError, "%s.isLoggedIn" % (type(self))

    def subscribe(self, name, remoteId):
        raise NotImplementedError, "%s.subscribe" % (type(self))

    def sync(self, share):
        raise NotImplementedError, "%s.sync" % (type(self))

    def _getClient(self):
        return type(self)._clients.get(self.itsUUID)

    def _setClient(self, client):
        type(self)._clients[self.itsUUID] = client

    client = property(_getClient, _setClient)
Пример #8
0
class Location(ContentItem):
    """
       @note: Location may not be calendar specific.
    """

    schema.kindInfo(displayName="Location", displayAttribute="displayName")

    eventsAtLocation = schema.Sequence(CalendarEventMixin,
                                       displayName="Calendar Events",
                                       inverse=CalendarEventMixin.location)

    def __str__(self):
        """
          User readable string version of this Location
        """
        if self.isStale():
            return super(Location, self).__str__()
            # Stale items can't access their attributes
        return self.getItemDisplayName()

    def getLocation(cls, view, locationName):
        """
          Factory Method for getting a Location.

          Lookup or create a Location based on the supplied name string.
        If a matching Location object is found in the repository, it
        is returned.  If there is no match, then a new item is created
        and returned.  
        @param locationName: name of the Location
        @type locationName: C{String}
        @return: C{Location} created or found
        """
        # make sure the locationName looks reasonable
        assert locationName, "Invalid locationName passed to getLocation factory"

        # get all Location objects whose displayName match the param
        its = Location.iterItems(view, exact=True)
        locQuery = [i for i in its if i.displayName == locationName]

        ##         locQuery = view.findPath('//Queries/calendarLocationQuery')
        ##         if locQuery is None:
        ##             queryString = u'for i in "//parcels/osaf/pim/calendar/Location" \
        ##                       where i.displayName == $0'
        ##             p = view.findPath('//Queries')
        ##             k = view.findPath('//Schema/Core/Query')
        ##             locQuery = Query.Query ('calendarLocationQuery', p, k, queryString)
        ##         locQuery.args["$0"] = ( locationName, )

        # return the first match found, if any
        for firstSpot in locQuery:
            return firstSpot

        # make a new Location
        newLocation = Location(view=view)
        newLocation.displayName = locationName
        return newLocation

    getLocation = classmethod(getLocation)
class AllIndexDefinitions(schema.Item):
    """
    Singleton item that hosts a reflist of all L{IndexDefinition}s
    in existance: L{IndexDefinition}'s constructor adds each new instance
    to us to assure this.
    """
    indexDefinitions = schema.Sequence(IndexDefinition,
                                       initialValue=[],
                                       inverse=schema.One())
Пример #10
0
class TrunkSubtree(schema.Item):
    """A mapping between an Item and the "root blocks" that should appear when
    an Item inheriting from that Kind is displayed. (A "root block" should
    have a "position" attribute to enable it to be sorted with other root
    blocks.)
    """

    key = schema.One(schema.Item, required=True)
    rootBlocks = schema.Sequence('Block', inverse='parentTrunkSubtrees')
Пример #11
0
class Principal(ContentItem):
    # @@@MOR These should be moved out so that authentication can be made
    # more general, but they're here for convenience for now.
    login = schema.One(schema.Text)
    password = schema.One(schema.Text)

    members = schema.Sequence(initialValue=[])
    memberOf = schema.Sequence(initialValue=[], inverse=members)

    def isMemberOf(self, pid):

        if self.itsUUID == pid:
            return True

        for member in getattr(self.itsView.findUUID(pid), 'members', []):
            if self.isMemberOf(member.itsUUID):
                return True

        return False
Пример #12
0
class TaskEventExtraMixin(items.ContentItem):
    """
      Task Event Extra Mixin is the bag of attributes that
    appears when you have an Item that is both a Task and a
    CalendarEvent.
    We only instantiate these Items when we "unstamp" an
    Item, to save the attributes for later "restamping".
    """

    schema.kindInfo(
        displayName = "Task Event Extra Mixin Kind",
        description =
            "The attributes specific to an item that is both a task and an "
            "event.  This is additional 'due by' information. "
    )

    dueByDate = schema.One(
        schema.DateTime,
        displayName = 'Due by Date',
        doc = 'The date when a Task Event is due.',
    )
    dueByRecurrence = schema.Sequence(
        'osaf.pim.calendar.Calendar.RecurrencePattern',
        displayName = 'Due by Recurrence',
        doc = 'Recurrence information for a Task Event.',
    )
    dueByTickler = schema.One(
        schema.DateTime,
        displayName = 'Due by Tickler',
        doc = 'The reminder information for a Task Event.',
    )

    def InitOutgoingAttributes (self):
        """ Init any attributes on ourself that are appropriate for
        a new outgoing item.
        """
        try:
            super(TaskEventExtraMixin, self).InitOutgoingAttributes ()
        except AttributeError:
            pass
        TaskEventExtraMixin._initMixin (self) # call our init, not the method of a subclass

    def _initMixin (self):
        """
          Init only the attributes specific to this mixin.
        Called when stamping adds these attributes, and from __init__ above.
        """
        # default the dueByDate to the task's dueDate
        try:
            self.dueByDate = self.dueDate
        except AttributeError:
            pass
Пример #13
0
class CollectionColors(schema.Item):
    """
    Temporarily put the CollectionColors here until we refactor collection
    to remove display information
    """
    colors = schema.Sequence(ColorType)
    colorIndex = schema.One(schema.Integer)

    def nextColor(self):
        color = self.colors[self.colorIndex]
        self.colorIndex += 1
        if self.colorIndex == len(self.colors):
            self.colorIndex = 0
        return color
Пример #14
0
class FlickrPhotoMixin(PhotoMixin):
    """
    A mixin that adds flickr attributes to a Note item
    """
    flickrID = schema.One(schema.Text)
    imageURL = schema.One(schema.URL)
    datePosted = schema.One(schema.DateTime)
    tags = schema.Sequence()
    owner = schema.One(schema.Text, indexed=True)

    schema.addClouds(sharing=schema.Cloud(
        literal=[owner, flickrID, imageURL, tags]))

    def _setPhoto(self, photo):
        if photo is not None:
            self.flickrID = photo.id
            self.displayName = photo.title
            self.description = photo.description.encode('utf8')
            self.owner = photo.owner.username
            if (photo.owner.realname is not None
                    and photo.owner.realname.strip()):
                self.owner = photo.owner.realname

            self.imageURL = URL(photo.getURL(urlType="source"))
            self.datePosted = datetime.utcfromtimestamp(int(photo.dateposted))
            self.dateTaken = dateutil.parser.parse(
                photo.datetaken,
                default=datetime.now(tz=self.itsView.tzinfo.default))
            if photo.tags:
                self.tags = [
                    Tag.getTag(self.itsView, tag.text) for tag in photo.tags
                ]

    photo = property(None, _setPhoto)

    def __setup__(self):
        self.importFromURL(self.imageURL)

    @schema.observer(owner)
    def onOwnerChange(self, op, attr):
        self.updateDisplayWho(op, attr)

    def addDisplayWhos(self, whos):
        super(FlickrPhotoMixin, self).addDisplayWhos(whos)
        owner = getattr(self, 'owner', None)
        if owner is not None:
            whos.append((16, owner, 'owner'))
Пример #15
0
class NotifyHandler(schema.Item):
    """
    An item that exists only to handle notifications
    we should change notifications to work on callables -- John is cool with that.
    """
    log = schema.Sequence(initialValue=[])
    collectionEventHandler = schema.One(schema.String, initialValue="onCollectionEvent")

    def checkLog(self, op, item, other):
        if len(self.log) == 0:
            return False
        rec = self.log[-1]
        return rec[0] == op and rec[1] == item and rec[2] == "rep" and rec[3] == other and rec[4] == ()
    

    def onCollectionEvent(self, op, item, name, other, *args):
        self.log.append((op, item, name, other, args))
Пример #16
0
class DynamicContainer(RefCollectionDictionary, DynamicBlock):
    """
      A block whose children are built dynamically, when the
    Active View changes.
    This list of children is in "dynamicChildren" and the
    back pointer is in "dynamicParent".
    """

    dynamicChildren = schema.Sequence(
        Block.Block, otherName = 'dynamicParent',
    )

    collectionSpecifier = schema.One(redirectTo = 'dynamicChildren')

    schema.addClouds(
        copying = schema.Cloud(byCloud = [dynamicChildren])
    )

    def itemNameAccessor(self, item):
        """
          Use blockName for the accessor
        """
        return item.blockName
    
    def isDynamicContainer (self):
        return True

    def populateFromStaticChildren (self):
        # copy our static children as a useful starting point
        self.dynamicChildren.clear ()
        for block in self.childrenBlocks:
            self[block.blockName] = block

    def ensureDynamicChildren (self):
        """
          Make sure we have a DynamicChildren hierarchy, since all my
        subclasses use that hierarchy when they synchronize.
        If there is no DynamicChildren built, then initialize it from
        the childrenBlocks hierarchy.
        """
        try:
            children = len (self.dynamicChildren)
        except AttributeError:
            children = 0
        if not children:
            self.populateFromStaticChildren ()
Пример #17
0
class TaskStamp(Stamp):
    """
    TaskStamp is the bag of Task-specific attributes.
    """

    schema.kindInfo(annotates = notes.Note)

    __use_collection__ = True
    
    requestor = schema.One(
        Contact,
        description =
            "Issues:\n"
            '   Type could be Contact, EmailAddress or String\n'
            '   Think about using the icalendar terminology\n',
        inverse = Contact.requestedTasks,
    )
    requestee = schema.Sequence(
        Contact,
        description =
            "Issues:\n"
            '   Type could be Contact, EmailAddress or String\n'
            '   Think about using the icalendar terminology\n',
        inverse = Contact.taskRequests,
    )

    # Redirections
    @apply
    def summary():
        def fget(self):
            return self.itsItem.displayName
        def fset(self, value):
            self.itsItem.displayName = value
        return schema.Calculated(schema.Text, (items.ContentItem.displayName,),
                                 fget, fset)

    schema.addClouds(
        copying = schema.Cloud(
            requestor, requestee
        )
    )

    def InitOutgoingAttributes (self):
        self.itsItem.InitOutgoingAttributes()
Пример #18
0
class Tag(pim.ContentItem):
    """
    Tag are items with bidirectional references to all the FlickrPhoto's
    with that tag. This makes it easy to find all photos with a given tag or
    all tags belonging to a photo. Currently, there isn't any code that
    takes advantage of Tags.
    """
    itemsWithTag = schema.Sequence(FlickrPhotoMixin,
                                   inverse=FlickrPhotoMixin.tags)

    @classmethod
    def getTag(cls, view, tagName):
        """
        Factory Method for getting a Tag.

        Lookup or create a Tag based on the supplied name string.

        If a matching Tag object is found in the repository, it
        is returned.  If there is no match, then a new item is created
        and returned.  

        @param tagName: name of the Tag
        @type tagName: C{String}
        @return: C{Tag} created or found
        """
        # make sure the tagName looks reasonable
        assert tagName, "Invalid tagName passed to getTag factory"

        # get all Tag objects whose displayName match the param
        # return the first match found, if any
        for i in Tag.iterItems(view, exact=True):
            if i.displayName == tagName:
                return i

        # make a new Tag
        newTag = Tag(itsView=view)
        newTag.displayName = tagName

        return newTag
Пример #19
0
class ListCollection(AbstractCollection):
    """
    """
    schema.kindInfo(
        displayName="ListCollection"
    )

    refCollection = schema.Sequence(otherName=Item.collections,initialValue=[])

    def __init__(self, *args, **kw):
        super(ListCollection, self).__init__(*args, **kw)
        self.rep = Set((self,'refCollection'))
        self.refCollection = []

    def add(self, item):
        self.refCollection.append(item)

    def remove(self, item):
        self.refCollection.remove(item)

    def contentsUpdated(self, item):
        self._collectionChanged('changed' , 'rep', item)
        pass
Пример #20
0
class StampCollection(ContentCollection):
    """
    A C{ContentCollection} to store the collection of items that have a
    particular C{Stamp}. Used in favor of C{ListCollection} because there
    is code (in recurrence, for example) that manipulates
    C{ContentItem.collections}, the inverse of C{ListCollection.inclusions}
    in a way that breaks stamp collection membership.
    """
    __metaclass__ = schema.CollectionClass
    __collection__ = 'stampedItems'

    schemaItem = schema.One(StampItem, inverse=StampItem._collection)
    stampedItems = schema.Sequence(Stamp,
                                   inverse=Stamp.stampCollections,
                                   initialValue=[])

    @property
    def stamp_type(self):
        return self.schemaItem.stampClass

    def __repr__(self):
        return "<%s(%s): %s>" % (type(self).__name__,
                                 self.schemaItem.stampClass,
                                 self.itsUUID.str16())
Пример #21
0
class Reminder(schema.Item):
    """
    The base class for reminders. Note that this only supports 'custom'
    (fixed-time) reminders; the more familiar relative reminders are
    defined in Calendar.py on as RelativeReminder. This resolves some
    unfortunate circular import dependency issues we had in the past.
    """
    # Make values we can use for distant (or invalid) reminder times
    farFuture = datetime.max
    if farFuture.tzinfo is None:
        farFuture = farFuture.replace(tzinfo=ICUtzinfo.getInstance('UTC'))
    distantPast = datetime.min
    if distantPast.tzinfo is None:
        distantPast = distantPast.replace(tzinfo=ICUtzinfo.getInstance('UTC'))
    
    absoluteTime = schema.One(
        schema.DateTimeTZ,
        defaultValue=None,
        doc="If set, overrides relativeTo as the base time for this reminder"
    )

    # These flags represent what kind of reminder this is:
    # userCreated promptUser
    #   True        True    a relative or absolute user reminder, usually
    #                       created in the detail view (or possibly, imported
    #                       as ICalendar)
    #   False       False   An internal reminder relative to the
    #                       the effectiveStartTime of an event, used
    #                       to update its triageStatus when it fires.
    #
    # @@@ [grant] For now, userCreated and promptUser are identical... It's
    # not so clear to me whether we need both.
    #
    
    userCreated = schema.One(
        schema.Boolean,
        defaultValue=True,
        doc="Is a user-created reminder?"
    )

    promptUser = schema.One(
        schema.Boolean,
        defaultValue=True,
        doc="Should we show this reminder to the user when it goes off?")
    
    reminderItem = schema.One(
        defaultValue=None,
    )

    pendingEntries = schema.Sequence(
        PendingReminderEntry,
        defaultValue=Empty,
        inverse=PendingReminderEntry.reminder,
        doc="What user-created reminders have fired, and not been " \
            "dismissed or snoozed?"
    )
    
    nextPoll = schema.One(
        schema.DateTimeTZ,
        doc="When next will something interesting happen with this reminder?" \
            "Set to reminder.farFuture if this reminder has expired.",
        defaultValue=None,
    )
    
    description = schema.One(
        schema.Text,
        doc="End-user text description of this reminder. Currently unused by "
            "Chandler.",
    )

    duration = schema.One(
        schema.TimeDelta,
        doc="Reminder DURATION (a la ICalendar VALARM); unused by Chandler.",
        defaultValue=timedelta(0),
    )
    repeat = schema.One(
        schema.Integer,
        doc="Reminder REPEAT (a la ICalendar VALARM); unused by Chandler.",
        defaultValue=0,
    )

    schema.addClouds(
        sharing = schema.Cloud(
            literal = [absoluteTime, userCreated, promptUser]
        ),
        copying = schema.Cloud(
            literal = [absoluteTime, userCreated, promptUser, nextPoll]
        )
    )

    def onItemDelete(self, view, deferring):
        if self.pendingEntries:
            pending = list(self.pendingEntries)
            self.pendingEntries.clear()
            for entry in pending:
                entry.delete(recursive=True)

    def updatePending(self, when=None):
        """
        The method makes sure that the Reminder's list of
        PendingReminderEntries is up-to-date. This involves adding new
        entries to the list, via the Remindable.reminderFired() method.
        Also, the C{nextPoll} attribute is updated to the next time
        something interesting will happen (i.e. another item should be
        reminded); this may be C{Reminder.farFuture} if the C{Reminder}
        has no more items to process.
        
        @param when: The time to update to. You can pass C{None}, in which
                     case C{datetime.now()} is used.
        @type when: C{datetime}.
        """
        if when is None:
            when = datetime.now(self.itsView.tzinfo.default)

        if self.nextPoll is None:
            # No value for nextPoll means we've just been initialized.
            # If we're in the past, we treat ourselves as expired.
                
            if self.absoluteTime >= when:
                self.nextPoll = self.absoluteTime
            else:
                self.nextPoll = self.farFuture
                
        if self.nextPoll != self.farFuture and when >= self.absoluteTime:
            self.reminderItem.reminderFired(self, self.absoluteTime)
            self.nextPoll = self.farFuture
            
        self._checkExpired()

    def _checkExpired(self):
        if self.isExpired() and not self.userCreated:
            self.delete(True)

    def dismissItem(self, item):
        pendingEntries = self.pendingEntries
        toDismiss = list(p for p in pendingEntries if p.item is item)
        assert len(toDismiss), "Attempt to dismiss item non-pending item %r" % (
                               item)

        for item in toDismiss:
            item.delete(recursive=True)
            
        if not self.pendingEntries:
            self.nextPoll = self.farFuture

        self._checkExpired()
                

    def snoozeItem(self, item, delta):
        nextPoll = self.nextPoll
        pendingEntries = self.pendingEntries
        toSnooze = list(p for p in pendingEntries if p.item is item)
        assert len(toSnooze), "Attempt to snooze item non-pending item %r" % (
                               item)

        when = datetime.now(self.itsView.tzinfo.default) + delta
        for item in toSnooze:
            item.when = when
            item.snoozed = True
            nextPoll = min(nextPoll, item.when)
            
        self.nextPoll = nextPoll
        
    def getItemBaseTime(self, item):
        """
        Return the time we would fire for this item (independent of
        if it has been snoozed?)
        """
        return self.absoluteTime or Reminder.farFuture
        
    getReminderTime = getItemBaseTime
        
    def isExpired(self):
        return (not self.pendingEntries and
                self.nextPoll is not None
                and self.nextPoll >= Reminder.farFuture)
        
        
    @schema.observer(reminderItem)
    def reminderItemChanged(self, op, attr):
        if op == 'remove':
            if self.pendingEntries:
                # @@@ [grant] Check this!
                toRemove = list(p for p in self.pendingEntries
                                if isDead(p.item))
            
                for pending in toRemove:
                    self.pendingEntries.remove(pending)
                    
            if not self.pendingEntries:
                self.delete(True)

    @classmethod
    def defaultTime(cls, view):
        """ 
        Something's creating a reminder and needs a default time.
        We'll return 5PM today if that's in the future; otherwise, 8AM tomorrow.
        """    
        # start with today at 5PM
        t = datetime.now(tz=view.tzinfo.default)\
            .replace(hour=17, minute=0, second=0, microsecond=0)
        now = datetime.now(tz=view.tzinfo.default)
        if t < now:
            # Make it tomorrow morning at 8AM (eg, 15 hours later)
            t += timedelta(hours=15)
        return t

    @classmethod
    def getPendingTuples(cls, view, when):
        """
        Return a list of all reminder tuples with fire times in the past, 
        sorted by reminder time.

        Each tuple contains (reminderTime, remindable, reminder).
        """

        allFutureReminders = schema.ns('osaf.pim', view).allFutureReminders
        
        remindersToPoll = []
        for reminder in allFutureReminders.iterindexvalues('reminderPoll'):
            if reminder.nextPoll is None or reminder.nextPoll <= when:
                remindersToPoll.append(reminder)
            else:
                break

        for reminder in remindersToPoll:
            reminder.updatePending(when)

        pendingKind = PendingReminderEntry.getKind(view)
        trash = schema.ns("osaf.pim", view).trashCollection
        resultTuples = []
        
        for entry in pendingKind.iterItems():
            thisTuple = tuple(getattr(entry, attr, None)
                               for attr in ('when', 'item', 'reminder'))
            # Show everything except reminders in the trash, and
            # reminders that have been snoozed into the future. Also,
            # don't return any "dead" items in the tuple, and check
            # for missing attributes (bug 11415).
            if (thisTuple[0] is not None and
                not isDead(thisTuple[1]) and
                not isDead(thisTuple[2]) and
                not thisTuple[1] in trash and
                not (entry.snoozed and thisTuple[0] > when)):

                resultTuples.append(thisTuple)
                                
        resultTuples.sort(key=lambda t: t[0])
        return resultTuples
Пример #22
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)
Пример #23
0
class RecurrenceRuleSet(items.ContentItem):
    """
    A collection of recurrence and exclusion rules, dates, and exclusion dates.
    """
    rrules = schema.Sequence(RecurrenceRule,
                             inverse=RecurrenceRule.rruleFor,
                             deletePolicy='cascade')
    exrules = schema.Sequence(RecurrenceRule,
                              inverse=RecurrenceRule.exruleFor,
                              deletePolicy='cascade')
    rdates = schema.Sequence(schema.DateTimeTZ, )
    exdates = schema.Sequence(schema.DateTimeTZ, )
    events = schema.Sequence()  # inverse of EventStamp.rruleset

    schema.addClouds(copying=schema.Cloud(rrules, exrules, rdates, exdates),
                     sharing=schema.Cloud(literal=[exdates, rdates],
                                          byCloud=[exrules, rrules]))

    @schema.observer(rrules, exrules, rdates, exdates)
    def onRuleSetChanged(self, op, name):
        """If the RuleSet changes, update the associated event."""
        if not getattr(self, '_ignoreValueChanges', False):
            if self.hasLocalAttributeValue('events'):
                pimNs = schema.ns("osaf.pim", self.itsView)
                for eventItem in self.events:
                    pimNs.EventStamp(eventItem).getFirstInRule().cleanRule()
                    # assume we have only one conceptual event per rrule
                    break

    def addRule(self, rule, rrulesorexrules='rrules'):
        """Add an rrule or exrule, defaults to rrule.

        @param rule: Rule to be added
        @type  rule: L{RecurrenceRule}

        @param rrulesorexrules: Whether the rule is an rrule or exrule
        @type  rrulesorexrules: 'rrules' or 'exrules'

        """
        try:
            getattr(self, rrulesorexrules).append(rule)
        except AttributeError:
            setattr(self, rrulesorexrules, [rule])

    def createDateUtilFromRule(self,
                               dtstart,
                               ignoreIsCount=True,
                               convertFloating=False,
                               ignoreShortFrequency=True):
        """Return an appropriate dateutil.rrule.rruleset.

        @param dtstart: The start time for the recurrence rule
        @type  dtstart: C{datetime}

        @param ignoreIsCount: Whether the isCount flag should be used to convert
                              until endtimes to a count. Converting to count
                              takes extra cycles and is only necessary when
                              the rule is going to be serialized
        @type  ignoreIsCount: C{bool}

        @param convertFloating: Whether or not to allow view.tzinfo.floating
                                in datetimes of the rruleset. If C{True},
                                naive datetimes are used instead. This is
                                needed for exporting floating events to
                                icalendar format.
        @type  convertFloating: C{bool}

        @param ignoreShortFrequency: If ignoreShortFrequency is True, replace
                                     hourly or more frequent rules with a single
                                     RDATE matching dtstart, so as to avoid 
                                     wasting millions of cycles on nonsensical
                                     recurrence rules.
        @type  ignoreShortFrequency: C{bool}

        @rtype: C{dateutil.rrule.rruleset}

        """
        view = self.itsView
        args = (ignoreIsCount, convertFloating)
        ruleset = rruleset()
        for rtype in 'rrule', 'exrule':
            for rule in getattr(self, rtype + 's', []):
                if ignoreShortFrequency and rule.freq in SHORT_FREQUENCIES:
                    # too-frequent rule, as Admiral Ackbar said, "IT'S A TRAP!"
                    ruleset.rdate(dtstart)
                    return ruleset
                rule_adder = getattr(ruleset, rtype)
                rule_adder(rule.createDateUtilFromRule(dtstart, *args))

        for datetype in 'rdate', 'exdate':
            for date in getattr(self, datetype + 's', []):
                if convertFloating and date.tzinfo == view.tzinfo.floating:
                    date = date.replace(tzinfo=None)
                else:
                    date = coerceTimeZone(view, date, dtstart.tzinfo)
                getattr(ruleset, datetype)(date)

        if (ignoreIsCount and not getattr(self, 'rrules', [])
                and getattr(self, 'rdates', [])):
            # no rrule, but there are RDATEs, create an RDATE for dtstart, or it
            # won't appear to be part of the rule
            ruleset.rdate(dtstart)

        return ruleset

    def setRuleFromDateUtil(self, ruleSetOrRule):
        """Extract rules and dates from ruleSetOrRule, set them in self.

        If a dateutil.rrule.rrule is passed in instead of an rruleset, treat
        it as the new rruleset.

        @param ruleSetOrRule: The rule to marshall into Chandler
        @type  ruleSetOrRule: C{dateutil.rrule.rrule} or C{dateutil.rrule.rruleset}

        """
        ignoreChanges = getattr(self, '_ignoreValueChanges', False)
        self._ignoreValueChanges = True
        try:
            if isinstance(ruleSetOrRule, rrule):
                set = rruleset()
                set.rrule(ruleSetOrRule)
                ruleSetOrRule = set
            elif not isinstance(ruleSetOrRule, rruleset):
                raise TypeError, "ruleSetOrRule must be an rrule or rruleset"
            for rtype in 'rrule', 'exrule':
                rules = getattr(ruleSetOrRule, '_' + rtype, [])
                if rules is None: rules = []
                itemlist = []
                for rule in rules:
                    ruleItem = RecurrenceRule(None, None, None, self.itsView)
                    ruleItem.setRuleFromDateUtil(rule)
                    itemlist.append(ruleItem)
                setattr(self, rtype + 's', itemlist)
            for typ in 'rdate', 'exdate':
                datetimes = [
                    forceToDateTime(self.itsView, d)
                    for d in getattr(ruleSetOrRule, '_' + typ, [])
                ]
                setattr(self, typ + 's', datetimes)
        finally:
            self._ignoreValueChanges = ignoreChanges
        # only one rule cleaning is useful when setting a new rule
        self.onRuleSetChanged(None, None)

    def isComplex(self):
        """Determine if the rule is too complex to display a meaningful
        description about it.

        @rtype: C{bool}

        """
        if hasattr(self, 'rrules'):
            if len(self.rrules) != 1:
                return True  # multiple rules
            for recurtype in 'exrules', 'rdates':
                if self.hasLocalAttributeValue(recurtype) and \
                       len(getattr(self, recurtype)) != 0:
                    return True  # more complicated rules
            rule = self.rrules.first()
            for attr in RecurrenceRule.listNames:
                if getattr(rule, attr) not in (None, []):
                    return True
            if rule.byweekday is not None:
                # treat nth weekday of the month as complex
                for daystruct in rule.byweekday:
                    if daystruct.selector != 0:
                        return True
            return False
        else:
            return True

    def isCustomRule(self):
        """Determine if this is a custom rule.

        For the moment, simple daily, weekly, or monthly repeating events,
        optionally with an UNTIL date, or the abscence of a rule, are the only
        rules which are not custom.

        @rtype: C{bool}

        """
        if self.isComplex():
            return True
        # isComplex has already tested for most custom things, but
        # not intervals greater than 1 and multiple weekdays
        rule = self.rrules.first()
        if not (rule.interval == 1 or
                (rule.interval == 2 and rule.freq == 'weekly')):
            return True
        elif rule.isWeekdayRule():
            return False
        elif rule.byweekday:
            return True
        else:
            return False

    def getCustomDescription(self):
        """Return a string describing custom rules.

        @rtype: C{str}

        """
        too_frequent = False
        if hasattr(self, 'rrules') and len(self.rrules) > 0:
            for rule in self.rrules:
                if rule.freq in SHORT_FREQUENCIES:
                    too_frequent = True
                    break

        if self.isComplex():
            if not too_frequent:
                return _(u"No description.")
            else:
                return _(u"Too frequent.")
        else:
            # Get the rule values we can interpret (so far...)
            rule = self.rrules.first()
            freq = rule.freq
            interval = rule.interval
            until = rule.calculatedUntil()
            days = rule.byweekday

            # Build the index and argument dictionary
            # The index must be built in the 'fxdus' order
            index = 'f'
            dct = {}

            if too_frequent:
                index += 'x'

            elif freq == 'weekly' and days is not None:
                index += 'd'
                daylist = [weekdayAbbrevMap[i.weekday] for i in days]
                dct['days'] = u" ".join(daylist)

            dct['frequencyplural'] = pluralFrequencyMap[freq]
            dct['frequencysingular'] = singularFrequencyMap[freq]
            dct['frequencyadverb'] = adverbFrequencyMap[freq]

            if not too_frequent and until is not None:
                index += 'u'
                formatter = DateFormat.createDateInstance(DateFormat.kShort)
                dct['date'] = unicode(formatter.format(until))

            if interval != 1:
                dct['interval'] = str(interval)
                index += 'p'
            else:
                index += 's'

            return descriptionFormat[index] % dct

    def transformDatesAfter(self, after, changeDate):
        """
        Transform dates (later than "after") in exdates, rdates and
        until, by applying the function C{changeDate}.

        @param after: Earliest date to move, or None for all occurrences
        @type  after: C{datetime} or None

        @param changeDate: Time difference
        @type  changeDate: C{callable}, taking a C{datetime} and
                           returning a C{datetime}


        """
        self._ignoreValueChanges = True
        for datetype in 'rdates', 'exdates':
            datelist = getattr(self, datetype, None)
            if datelist is not None:
                l = []
                for dt in datelist:
                    if after is None or dt >= after:
                        l.append(changeDate(dt))
                    else:
                        l.append(dt)
                setattr(self, datetype, l)

        for rule in self.rrules or []:
            if not rule.untilIsDate:
                try:
                    until = rule.until
                except AttributeError:
                    pass
                else:
                    if after is None or until >= after:
                        rule.until = changeDate(until)

        del self._ignoreValueChanges

    def removeDates(self, cmpFn, endpoint):
        """Remove dates in exdates and rdates before or after endpoint.

        @param cmpFn: Comparison function (will be called with two
                      C{datetime} objects as arguments).
        @type  cmpFn: callable

        @param endpoint: Start or end point for comparisons
        @type  endpoint: C{datetime}

        """
        for datetype in 'rdates', 'exdates':
            datelist = getattr(self, datetype, None)
            if datelist is not None:
                # need to start from the end, bug 11005
                for i in reversed(xrange(len(datelist))):
                    if cmpFn(datelist[i], endpoint):
                        self._ignoreValueChanges = True
                        del datelist[i]
                        self._ignoreValueChanges = False

    def moveRuleEndBefore(self, dtstart, end):
        """Make self's rules end before end.

        @param dtstart: Start time for the recurrence rule
        @type  dtstart: C{datetime}

        @param end: Date not to include in the rule's new end
        @type  end: C{datetime}

        """
        #change the rule, onRuleSetChanged will trigger cleanRule for master
        for rule in getattr(self, 'rrules', []):
            if (not rule.hasLocalAttributeValue('until')
                    or (rule.calculatedUntil() >= end)):
                rule.moveUntilBefore(dtstart, end)
        self.removeDates(datetime.__ge__, end)
Пример #24
0
class RecurrenceRule(items.ContentItem):
    """One rule defining recurrence for an item."""
    freq = schema.One(FrequencyEnum, defaultValue="weekly")
    isCount = schema.One(
        schema.Boolean,
        doc="If True, calculate and export count instead of until",
        defaultValue=False)
    until = schema.One(schema.DateTimeTZ, )
    untilIsDate = schema.One(
        schema.Boolean,
        doc="If True, treat until as an inclusive date, use until + 23:59 "
        "for until",
        defaultValue=True)
    interval = schema.One(schema.Integer, defaultValue=1)
    wkst = schema.One(WeekdayEnum, defaultValue=None)
    bysetpos = schema.Sequence(schema.Integer, defaultValue=None)
    bymonth = schema.Sequence(schema.Integer, defaultValue=None)
    bymonthday = schema.Sequence(schema.Integer, defaultValue=None)
    byyearday = schema.Sequence(schema.Integer, defaultValue=None)
    byweekno = schema.Sequence(schema.Integer, defaultValue=None)
    byweekday = schema.Sequence(WeekdayAndPositionStruct, defaultValue=None)
    byhour = schema.Sequence(schema.Integer, defaultValue=None)
    byminute = schema.Sequence(schema.Integer, defaultValue=None)
    bysecond = schema.Sequence(schema.Integer, defaultValue=None)
    rruleFor = schema.One()  # inverse of RecurrenceRuleSet.rruleset
    exruleFor = schema.One()  # inverse of RecurrenceRuleSet.exrules

    schema.addClouds(sharing=schema.Cloud(literal=[
        freq, isCount, until, untilIsDate, interval, wkst, bysetpos, bymonth,
        bymonthday, byyearday, byweekno, byweekday, byhour, byminute, bysecond
    ]))

    normalNames = "interval", "until"
    listNames = ("bysetpos", "bymonth", "bymonthday", "byyearday", "byweekno",
                 "byhour", "byminute", "bysecond")
    specialNames = "wkst", "byweekday", "freq"

    notSpecialNames = ("interval", "until", "bysetpos", "bymonth",
                       "bymonthday", "byyearday", "byweekno", "byhour",
                       "byminute", "bysecond")

    @schema.observer(interval, until, bysetpos, bymonth, bymonthday, byyearday,
                     byweekno, byhour, byminute, bysecond, wkst, byweekday,
                     freq)
    def onRecurrenceChanged(self, op, name):
        """If the rule changes, update any associated events."""
        for ruletype in ('rruleFor', 'exruleFor'):
            if self.hasLocalAttributeValue(ruletype):
                getattr(self, ruletype).onRuleSetChanged(op, 'rrules')

    # dateutil automatically sets these from dtstart, we don't want these
    # unless their length is greater than 1.
    interpretedNames = "byhour", "byminute", "bysecond"

    WEEKDAYS = ('monday', 'tuesday', 'wednesday', 'thursday', 'friday')

    def isWeekdayRule(self):
        if (self.freq == 'weekly' and self.interval == 1
                and len(self.byweekday or []) == len(self.WEEKDAYS)
                and set(self.WEEKDAYS) == set(
                    x.weekday for x in self.byweekday if x.selector == 0)):
            return True

    def calculatedUntil(self):
        """
        Return until or until + 23:59, depending on untilIsDate.
        Will return None if there's no 'until' (so don't assume you can
        compare this value with a datetime directly!)

        @rtype: C{datetime} or C{None}

        """
        try:
            until = self.until
        except AttributeError:
            return None

        if self.untilIsDate:
            return until.replace(hour=23, minute=59)
        else:
            return until

    def createDateUtilFromRule(self,
                               dtstart,
                               ignoreIsCount=True,
                               convertFloating=False):
        """Return an appropriate dateutil.rrule.rrule.

        @param dtstart: The start time for the recurrence rule
        @type  dtstart: C{datetime}

        @param ignoreIsCount: Whether the isCount flag should be used to convert
                              until endtimes to a count. Converting to count
                              takes extra cycles and is only necessary when
                              the rule is going to be serialized
        @type  ignoreIsCount: C{bool}

        @param convertFloating: Whether or not to allow view.tzinfo.floating
                                in datetimes of the rruleset. If C{True},
                                naive datetimes are used instead. This is
                                needed for exporting floating events to
                                icalendar format.
        @type  convertFloating: C{bool}

        @rtype: C{dateutil.rrule.rrule}

        """

        tzinfo = dtstart.tzinfo
        view = self.itsView

        def coerceIfDatetime(value):
            if isinstance(value, datetime):
                if convertFloating and tzinfo == view.tzinfo.floating:
                    value = coerceTimeZone(view, value,
                                           tzinfo).replace(tzinfo=None)
                else:
                    value = coerceTimeZone(view, value, tzinfo)
            return value

        # TODO: more comments
        kwargs = dict(
            (k, getattr(self, k, None)) for k in self.notSpecialNames)
        for key in self.specialNames:
            value = coerceIfDatetime(getattr(self, key))
            if value is not None:
                kwargs[key] = toDateUtil(value)
        if hasattr(self, 'until'):
            kwargs['until'] = coerceIfDatetime(self.calculatedUntil())
        rule = rrule(dtstart=coerceIfDatetime(dtstart), **kwargs)
        if ignoreIsCount or not self.isCount or not hasattr(self, 'until'):
            return rule
        else:
            # modifying in place may screw up cache, fix when we turn
            # on caching
            rule._count = rule.count()
            rule._until = None
            return rule

    def setRuleFromDateUtil(self, rrule):
        """Extract attributes from rrule, set them in self.

        @param rrule: The rule to marshall into Chandler
        @type  rrule: C{dateutil.rrule.rrule}

        """
        view = self.itsView
        self.untilIsDate = False
        until = None  # assume no limit
        if rrule._count is not None:
            self.isCount = True
            until = rrule[-1]
        self.wkst = fromDateUtilWeekday(rrule._wkst)
        self.freq = fromDateUtilFrequency(rrule._freq)

        # ignore byweekday if freq is WEEKLY and day correlates with dtstart
        # because it was automatically set by dateutil
        if rrule._freq != dateutil.rrule.WEEKLY or \
           len(rrule._byweekday or ()) != 1 or \
           rrule._dtstart.weekday() != rrule._byweekday[0]:
            listOfDayTuples = []
            if rrule._byweekday:
                # Day tuples are (dayOrdinal, n-th week of the month),
                # 0 means all weeks
                listOfDayTuples = [(day, 0) for day in rrule._byweekday]
            if rrule._bynweekday is not None:
                listOfDayTuples.extend(rrule._bynweekday)
            if len(listOfDayTuples) > 0:
                self.byweekday = []
                for day, n in listOfDayTuples:
                    day = fromDateUtilWeekday(day)
                    self.byweekday.append(WeekdayAndPositionStruct(day, n))
        if rrule._until is not None:
            until = rrule._until
        if rrule._interval != 1:
            self.interval = rrule._interval
        if until is None:
            if self.hasLocalAttributeValue('until'):
                del self.until
        else:
            if until.tzinfo is None:
                self.until = until.replace(tzinfo=view.tzinfo.floating)
            else:
                self.until = coerceTimeZone(view, until, view.tzinfo.default)

        for key in self.listNames:
            # TODO: cache getattr(rrule, '_' + key)
            value = getattr(rrule, '_' + key)
            if key == 'bymonthday':
                if value is not None:
                    value += (rrule._bynmonthday or ())
            if value is not None and \
               (key not in self.interpretedNames or \
               len(value) > 1):
                # cast tuples to list
                setattr(self, key, list(value))
        # bymonthday and bymonth may be set automatically by dateutil, if so,
        # unset them
        if rrule._freq in (dateutil.rrule.MONTHLY, dateutil.rrule.YEARLY):
            if len(rrule._bymonthday) == 1 and len(rrule._bynmonthday) == 0:
                if rrule._bymonthday[0] == rrule._dtstart.day:
                    del self.bymonthday
        if rrule._freq == dateutil.rrule.YEARLY:
            if len(rrule._bymonth or ()) == 1 and \
                   rrule._byweekday is None and \
                   len(rrule._bynweekday or ()) == 0:
                if rrule._bymonth[0] == rrule._dtstart.month:
                    del self.bymonth

    def getPreviousRecurrenceID(self, dtstart, recurrenceID):
        """Return the date of the previous recurrenceID, or None.

        @param dtstart: The start time for the recurrence rule
        @type  dtstart: C{datetime}

        @param recurrenceID: The current recurrenceID
        @type  recurrenceID: C{datetime}

        @rtype: C{datetime} or C{None}
        """
        return self.createDateUtilFromRule(dtstart).before(recurrenceID)

    def moveUntilBefore(self, dtstart, recurrenceID):
        """Find the previous recurrenceID, set UNTIL to match it.

        @param dtstart: The start time for the recurrence rule
        @type  dtstart: C{datetime}

        @param recurrenceID: The current recurrenceID
        @type  recurrenceID: C{datetime}

        """
        previous = self.getPreviousRecurrenceID(dtstart, recurrenceID)
        assert previous is not None

        self.until = previous
        self.untilIsDate = False
Пример #25
0
class Server(schema.Item):
    """
         The web server Kind.  Instances of this Kind are found via KindQuery
         at startup and activated.  You may define a server item in your own
         parcel, and it will run as well as the default one defined in the
         webserver/servers parcel.
    """

    port = schema.One(schema.Integer,
                      displayName="Port",
                      doc="The port to listen on")

    path = schema.One(
        schema.String,
        displayName="Path",
        doc="The filesystem path pointing to the server's doc root.  This "
        "path is relative to the directory of the parcel.xml that "
        "defines the server item")

    resources = schema.Sequence(
        initialValue=(),
        displayName="Resources",
        doc="You may define custom twisted resources and associate them "
        "with this server")

    directories = schema.Sequence(
        initialValue=(),
        displayName="Directories",
        doc="You may specify other file system directories which will be "
        "used to server specific URL locations.  (See the Directory "
        "Kind)")

    def startup(self):
        parcel = application.Parcel.Manager.getParentParcel(self)
        parcelDir = os.path.dirname(parcel.file)
        docRoot = os.path.join(parcelDir, self.path)
        root = static.File(docRoot)

        # .rpy files are twisted's version of a cgi
        root.ignoreExt(".rpy")
        root.processors = {".rpy": script.ResourceScript}

        logger.info("Activating web server on port %s with docroot %s" % \
         (str(self.port), str(docRoot)))

        # Hook up all associated resources to a location under the docroot
        for res in self.resources:
            logger.info("   Hooking up /%s to resource '%s'" % \
             (str(res.location), str(res.displayName)))
            resourceInstance = res.getResource()
            # Give the main thread repository view to the resource instance
            resourceInstance.repositoryView = self.itsView
            root.putChild(res.location, resourceInstance)

        # Hook up all associated directories to a location under the docroot
        for directory in self.directories:
            # First, find this directory's parcel, then determine that parcel's
            # directory, then join the directory.path.
            parcel = application.Parcel.Manager.getParentParcel(directory)
            parcelDir = os.path.dirname(parcel.file)
            docRoot = os.path.join(parcelDir, directory.path)
            logger.info("   Hooking up /%s to directory %s" % \
             (str(directory.location), str(docRoot)))
            root.putChild(directory.location, static.File(docRoot))

        site = server.Site(root)
        try:
            reactor.callFromThread(reactor.listenTCP, self.port, site)
            self.activated = True
        except twisted.internet.error.CannotListenError, e:
            logger.error("Twisted error: %s" % str(e))
            print e
Пример #26
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)
Пример #27
0
class RecurrenceRuleSet(items.ContentItem):
    rrules = schema.Sequence(RecurrenceRule,
                             displayName="Recurrence rules",
                             inverse=RecurrenceRule.rruleFor,
                             deletePolicy='cascade')
    exrules = schema.Sequence(RecurrenceRule,
                              displayName="Exclusion rules",
                              inverse=RecurrenceRule.exruleFor,
                              deletePolicy='cascade')
    rdates = schema.Sequence(schema.DateTime, displayName="Recurrence Dates")
    exdates = schema.Sequence(schema.DateTime, displayName="Exclusion Dates")
    events = schema.Sequence("osaf.pim.calendar.Calendar.CalendarEventMixin",
                             displayName="Events",
                             inverse="rruleset")

    schema.addClouds(copying=schema.Cloud(rrules, exrules, rdates, exdates))

    def addRule(self, rule, rruleorexrule='rrule'):
        """Add an rrule or exrule, defaults to rrule."""
        rulelist = getattr(self, rruleorexrule + 's', [])
        rulelist.append(rule)
        setattr(self, rruleorexrule + 's', rulelist)

    def createDateUtilFromRule(self, dtstart):
        """Return an appropriate dateutil.rrule.rruleset."""
        ruleset = rruleset()
        for rtype in 'rrule', 'exrule':
            for rule in getattr(self, rtype + 's', []):
                getattr(ruleset, rtype)(rule.createDateUtilFromRule(dtstart))
        for datetype in 'rdate', 'exdate':
            for date in getattr(self, datetype + 's', []):
                getattr(ruleset, datetype)(date)
        return ruleset

    def setRuleFromDateUtil(self, ruleSetOrRule):
        """Extract rules and dates from ruleSetOrRule, set them in self.
        
        If a dateutil.rrule.rrule is passed in instead of an rruleset, treat
        it as the new rruleset.
        
        """
        if isinstance(ruleSetOrRule, rrule):
            set = rruleset()
            set.rrule(ruleSetOrRule)
            ruleSetOrRule = set
        elif not isinstance(ruleSetOrRule, rruleset):
            raise TypeError, "ruleSetOrRule must be an rrule or rruleset"
        for rtype in 'rrule', 'exrule':
            rules = getattr(ruleSetOrRule, '_' + rtype, [])
            if rules is None: rules = []
            itemlist = []
            for rule in rules:
                ruleItem = RecurrenceRule(parent=self)
                ruleItem.setRuleFromDateUtil(rule)
                itemlist.append(ruleItem)
            setattr(self, rtype + 's', itemlist)
        for typ in 'rdate', 'exdate':
            # While most dates are naive, strip tzinfo off
            naive = [stripTZ(e) for e in getattr(ruleSetOrRule, '_' + typ, [])]
            setattr(self, typ + 's', naive)

    def isCustomRule(self):
        """Determine if this is a custom rule.
        
        For the moment, simple daily, weekly, or monthly repeating events, 
        optionally with an UNTIL date, or the abscence of a rule, are the only
        rules which are not custom.
        
        """
        if self.hasLocalAttributeValue('rrules'):
            if len(self.rrules) > 1:
                return True  # multiple rules
            for recurtype in 'exrules', 'rdates', 'exdates':
                if self.hasLocalAttributeValue(recurtype) and \
                       len(getattr(self, recurtype)) != 0:
                    return True  # more complicated rules
            rule = list(self.rrules)[0]
            if rule.interval != 1:
                return True
            for attr in RecurrenceRule.listNames + ("byweekday", ):
                if getattr(rule, attr):
                    return True
        return False

    def getCustomDescription(self):
        """Return a string describing custom rules."""
        return "not yet implemented"

    def onValueChanged(self, name):
        """If the RuleSet changes, update the associated event."""
        if name in ('rrules', 'exrules', 'rdates', 'exdates'):
            if self.hasLocalAttributeValue('events'):
                for event in self.events:
                    event.getFirstInRule().cleanRule()
                    # assume we have only one conceptual event per rrule
                    break
Пример #28
0
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
Пример #29
0
class RecurrenceRule(items.ContentItem):
    """One rule defining recurrence for an item."""
    freq = schema.One(FrequencyEnum,
                      displayName="Frequency possibilities",
                      defaultValue="weekly")
    isCount = schema.One(
        schema.Boolean,
        displayName="isCount",
        doc="If True, calculate and export count instead of until",
        defaultValue=False)
    until = schema.One(schema.DateTime, displayName="Until", defaultValue=None)
    untilIsDate = schema.One(
        schema.Boolean,
        displayName="untilIsDate",
        doc="If True, treat until as an inclusive date, use until + 23:59 "
        "for until",
        defaultValue=True)
    interval = schema.One(schema.Integer,
                          displayName="Interval",
                          defaultValue=1)
    wkst = schema.One(WeekdayEnum,
                      displayName="Week Start Day",
                      defaultValue=None)
    bysetpos = schema.Sequence(schema.Integer,
                               displayName="Position selector",
                               defaultValue=None)
    bymonth = schema.Sequence(schema.Integer,
                              displayName="Month selector",
                              defaultValue=None)
    bymonthday = schema.Sequence(schema.Integer,
                                 displayName="Ordinal day of month selector",
                                 defaultValue=None)
    byyearday = schema.Sequence(schema.Integer,
                                displayName="Ordinal day of year selector",
                                defaultValue=None)
    byweekno = schema.Sequence(schema.Integer,
                               displayName="Week number selector",
                               defaultValue=None)
    byweekday = schema.Sequence(WeekdayAndPositionStruct,
                                displayName="Weekday selector",
                                defaultValue=None)
    byhour = schema.Sequence(schema.Integer,
                             displayName="Hour selector",
                             defaultValue=None)
    byminute = schema.Sequence(schema.Integer,
                               displayName="Minute selector",
                               defaultValue=None)
    bysecond = schema.Sequence(schema.Integer,
                               displayName="Second selector",
                               defaultValue=None)
    rruleFor = schema.One('RecurrenceRuleSet', inverse='rrules')
    exruleFor = schema.One('RecurrenceRuleSet', inverse='exrules')

    normalNames = "interval", "until"
    listNames = "bysetpos", "bymonth", "bymonthday", "byyearday", "byweekno",\
                "byhour", "byminute", "bysecond"
    specialNames = "wkst", "byweekday", "freq"

    # dateutil automatically sets these from dtstart, we don't want these
    # unless their length is greater than 1.
    interpretedNames = "byhour", "byminute", "bysecond"

    def calculatedUntil(self):
        """Return until or until + 23:59, depending on untilIsDate."""
        if self.untilIsDate:
            if self.until is None:
                return None
            else:
                return self.until.replace(hour=23, minute=59)
        else:
            return self.until

    def createDateUtilFromRule(self, dtstart):
        """Return an appropriate dateutil.rrule.rrule."""
        kwargs = dict(
            (k, getattr(self, k)) for k in self.listNames + self.normalNames)
        for key in self.specialNames:
            if getattr(self, key) is not None:
                kwargs[key] = toDateUtil(getattr(self, key))
        if self.until is not None and self.untilIsDate:
            kwargs['until'] = self.calculatedUntil()
        rule = rrule(dtstart=dtstart, **kwargs)
        if not self.isCount or self.until is None:
            return rule
        else:
            # modifying in place may screw up cache, fix when we turn
            # on caching
            rule._count = rule.count()
            rule._until = None
            return rule

    def setRuleFromDateUtil(self, rrule):
        """Extract attributes from rrule, set them in self."""
        self.untilIsDate = False
        if rrule._count is not None:
            self.isCount = True
            # While most dates are naive, strip tzinfo off
            self.until = stripTZ(rrule[-1])
        self.wkst = fromDateUtilWeekday(rrule._wkst)
        self.freq = fromDateUtilFrequency(rrule._freq)

        # ignore byweekday if freq is WEEKLY and day correlates with dtstart
        # because it was automatically set by dateutil
        if rrule._freq is not dateutil.rrule.WEEKLY or \
           len(rrule._byweekday) != 1 or \
           rrule._dtstart.weekday() != rrule._byweekday[0]:
            listOfDayTuples = []
            if rrule._byweekday:
                # Day tuples are (dayOrdinal, n-th week of the month),
                # 0 means all weeks
                listOfDayTuples = [(day, 0) for day in rrule._byweekday]
            if rrule._bynweekday:
                listOfDayTuples.extend(tup for tup in rrule._bynweekday)
            if len(listOfDayTuples) > 0:
                self.byweekday = []
                for day, n in listOfDayTuples:
                    day = fromDateUtilWeekday(day)
                    self.byweekday.append(WeekdayAndPositionStruct(day, n))
        # While most dates are naive, strip tzinfo off
        if rrule._until is not None:
            self.until = stripTZ(rrule._until)
        if rrule._interval != 1:
            self.interval = rrule._interval

        for key in self.listNames:
            if getattr(rrule, '_' + key) is not None and \
                                        (key not in self.interpretedNames or \
                                         len(getattr(rrule, '_' + key)) > 1):
                # cast tuples to list, or will the repository do this for us?
                setattr(self, key, list(getattr(rrule, '_' + key)))
        # bymonthday and bymonth may be set automatically by dateutil, if so,
        # unset them
        if rrule._freq in (dateutil.rrule.MONTHLY, dateutil.rrule.YEARLY):
            if len(rrule._bymonthday) == 1:
                if rrule._bymonthday[0] == rrule._dtstart.day:
                    del self.bymonthday
        if rrule._freq == dateutil.rrule.YEARLY:
            if len(rrule._bymonth) == 1:
                if rrule._bymonth[0] == rrule._dtstart.month:
                    del self.bymonth

    def onValueChanged(self, name):
        """If the rule changes, update any associated events."""
        if name in self.listNames + self.normalNames + self.specialNames:
            for ruletype in ('rruleFor', 'exruleFor'):
                if self.hasLocalAttributeValue(ruletype):
                    getattr(self, ruletype).onValueChanged('rrules')
Пример #30
0
class User(schema.Item, Principal):

    name = schema.One(schema.Text)
    accounts = schema.Sequence(otherName='user', initialValue=[])