def __init__(self, parent=None): super(VAlarm, self).__init__(parent=parent) self.mAction = definitions.eAction_VAlarm_Display self.mTriggerAbsolute = False self.mTriggerOnStart = True self.mTriggerOn = DateTime() # Set duration default to 1 hour self.mTriggerBy = Duration() self.mTriggerBy.setDuration(60 * 60) # Does not repeat by default self.mRepeats = 0 self.mRepeatInterval = Duration() self.mRepeatInterval.setDuration(5 * 60) # Five minutes # Status self.mStatusInit = False self.mAlarmStatus = definitions.eAlarm_Status_Pending self.mLastTrigger = DateTime() self.mNextTrigger = DateTime() self.mDoneCount = 0 # Create action data self.mActionData = VAlarm.VAlarmDisplay("")
def getCacheEntry(cls, calresource, useruid, timerange): key = str(calresource.id()) + "/" + useruid token = (yield calresource.syncToken()) entry = (yield cls.fbcacher.get(key)) if entry: # Offset one day at either end to account for floating entry_timerange = Period.parseText(entry.timerange) cached_start = entry_timerange.getStart() + Duration( days=cls.CACHE_DAYS_FLOATING_ADJUST) cached_end = entry_timerange.getEnd() - Duration( days=cls.CACHE_DAYS_FLOATING_ADJUST) # Verify that the requested time range lies within the cache time range if compareDateTime(timerange.getEnd(), cached_end) <= 0 and compareDateTime( timerange.getStart(), cached_start) >= 0: # Verify that cached entry is still valid if token == entry.token: returnValue(entry.fbresults) returnValue(None)
def _getMasterEventDetails(self, component): """ Logic here comes from RFC4791 Section 9.9 """ start = component.getStartDateUTC() if start is None: return None rulestart = component.propertyValue("DTSTART") end = component.getEndDateUTC() duration = None if end is None: if not start.isDateOnly(): # Timed event with zero duration duration = Duration(days=0) else: # All day event default duration is one day duration = Duration(days=1) end = start + duration else: duration = differenceDateTime(start, end) return ( rulestart, start, end, duration, )
def sample(self): offset = PyDuration(seconds=int(self._helperDistribution.sample())) beginning = self.now(Timezone(tzid=self._tzname)) while offset: start, end = self._findWorkAfter(beginning) if end - start > offset: result = start + offset result.setMinutes(result.getMinutes() // 15 * 15) result.setSeconds(0) return result offset.setDuration(offset.getTotalSeconds() - (end - start).getTotalSeconds()) beginning = end
def time(self, component): """ Examples: 3:30 PM to 4:30 PM PDT All day 3:30 PM PDT 3:30 PM PDT to 7:30 PM EDT 1 day 2 days 1 day 1 hour 1 day 4 hours 18 minutes """ # Bind to '_' so pygettext.py will pick this up for translation _ = self.translation.ugettext tzStart = tzEnd = None dtStart = component.propertyValue("DTSTART") if dtStart.isDateOnly(): return ("", _("All day")) else: tzStart = dtStart.timeZoneDescriptor() dtEnd = component.propertyValue("DTEND") if dtEnd: if not dtEnd.isDateOnly(): tzEnd = dtEnd.timeZoneDescriptor() duration = dtEnd - dtStart else: tzEnd = tzStart duration = component.propertyValue("DURATION") if duration: dtEnd = dtStart + duration else: if dtStart.isDateOnly(): dtEnd = None duration = Duration(days=1) else: dtEnd = dtStart + Duration(days=1) dtEnd.setHHMMSS(0, 0, 0) duration = dtEnd - dtStart if dtStart == dtEnd: return (self.dtTime(dtStart), "") return (_("%(startTime)s to %(endTime)s") % { 'startTime': self.dtTime(dtStart, includeTimezone=(tzStart != tzEnd)), 'endTime': self.dtTime(dtEnd), }, self.dtDuration(duration))
def testRelaxedBad(self): test_relaxed_data = ( ("P12DT", 12 * 24 * 60 * 60, "P12D"), ("-P1WT", -7 * 24 * 60 * 60, "-P1W"), ("-P1W1D", -7 * 24 * 60 * 60, "-P1W"), ) for text, seconds, result in test_relaxed_data: ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_FIX self.assertEqual(Duration.parseText(text).getTotalSeconds(), seconds) self.assertEqual(Duration.parseText(text).getText(), result) ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_RAISE self.assertRaises(ValueError, Duration.parseText, text)
def _addEvent(self): # Don't perform any operations until the client is up and running if not self._client.started: return succeed(None) calendar = self._getRandomCalendarOfType('VEVENT') if not calendar: # No VEVENT calendars, so no new event... return succeed(None) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def testRelaxedBad(self): test_relaxed_data = ( ("P12DT", 12 * 24 * 60 * 60, "P12D"), ("-P1WT", -7 * 24 * 60 * 60, "-P1W"), ("-P1W1D", -7 * 24 * 60 * 60, "-P1W"), ) for text, seconds, result in test_relaxed_data: ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_FIX self.assertEqual( Duration.parseText(text).getTotalSeconds(), seconds) self.assertEqual(Duration.parseText(text).getText(), result) ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_RAISE self.assertRaises(ValueError, Duration.parseText, text)
def _addEvent(self): if not self._client.started: return succeed(None) calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT") while calendars: calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def _initEvent(self): if not self._client.started: return succeed(None) # If it already exists, don't re-create calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] if calendar.events: events = [ event for event in calendar.events.values() if event.url.endswith("event_to_update.ics") ] if events: return succeed(None) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) href = '%s%s' % (calendar.url, "event_to_update.ics") d = self._client.addEvent(href, vcalendar) return self._newOperation("create", d)
def __init__(self, start=None, end=None, duration=None): self.mStart = start if start is not None else DateTime() if end is not None: self.mEnd = end self.mDuration = self.mEnd - self.mStart self.mUseDuration = False elif duration is not None: self.mDuration = duration self.mEnd = self.mStart + self.mDuration self.mUseDuration = True else: self.mEnd = self.mStart.duplicate() self.mDuration = Duration() self.mUseDuration = False
def _initEvent(self): # Don't perform any operations until the client is up and running if not self._client.started: return succeed(None) try: calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0] except IndexError: # There is no calendar return succeed(None) self.myEventHref = '{}{}.ics'.format(calendar.url, str(uuid4())) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) vevent.replaceProperty(Property("DESCRIPTION", "AlarmAcknowledger")) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) d = self._client.addEvent(self.myEventHref, vcalendar) return self._newOperation("create", d)
def _invite(self): """ Try to add a new event, or perhaps remove an existing attendee from an event. @return: C{None} if there are no events to play with, otherwise a L{Deferred} which fires when the attendee change has been made. """ if not self._client.started: return succeed(None) # Find calendars which are eligible for invites calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT") while calendars: # Pick one at random from which to try to create an event # to modify. calendar = self.random.choice(calendars) calendars.remove(calendar) # Copy the template event and fill in some of its fields # to make a new event to create on the calendar. vcalendar = self._eventTemplate.duplicate() vevent = vcalendar.mainComponent() uid = str(uuid4()) dtstart = self._eventStartDistribution.sample() dtend = dtstart + Duration( seconds=self._eventDurationDistribution.sample()) vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC())) vevent.replaceProperty(Property("DTSTART", dtstart)) vevent.replaceProperty(Property("DTEND", dtend)) vevent.replaceProperty(Property("UID", uid)) rrule = self._recurrenceDistribution.sample() if rrule is not None: vevent.addProperty(Property(None, None, None, pycalendar=rrule)) vevent.addProperty(self._client._makeSelfOrganizer()) vevent.addProperty(self._client._makeSelfAttendee()) attendees = list(vevent.properties('ATTENDEE')) for _ignore in range(int(self._inviteeCountDistribution.sample())): try: self._addAttendee(vevent, attendees) except CannotAddAttendee: self._failedOperation("invite", "Cannot add attendee") return succeed(None) href = '%s%s.ics' % (calendar.url, uid) d = self._client.addInvite(href, vcalendar) return self._newOperation("invite", d)
def testGenerate(self): def _doTest(d, result): if result[0] == "+": result = result[1:] os = StringIO() d.generate(os) self.assertEqual(os.getvalue(), result) for seconds, result in TestDuration.test_data: _doTest(Duration(duration=seconds), result)
def testSetUseDuration(self): p1 = Period( start=DateTime(2000, 1, 1, 0, 0, 0), end=DateTime(2000, 1, 1, 1, 0, 0), ) p1.setUseDuration(True) self.assertTrue(p1.getText(), "20000101T000000/PT1H") p2 = Period( start=DateTime(2000, 1, 1, 0, 0, 0), duration=Duration(hours=1), ) p2.setUseDuration(False) self.assertTrue(p2.getText(), "20000101T000000/20000101T010000")
def getCacheEntry(cls, calresource, useruid, timerange): key = calresource.resourceID() + "/" + useruid token = (yield calresource.getInternalSyncToken()) entry = (yield fbcacher.get(key)) if entry: # Offset one day at either end to account for floating cached_start = entry.timerange.start + Duration( days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST) cached_end = entry.timerange.end - Duration( days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST) # Verify that the requested timerange lies within the cache timerange if compareDateTime(timerange.end, cached_end) <= 0 and compareDateTime( timerange.start, cached_start) >= 0: # Verify that cached entry is still valid if token == entry.token: returnValue(entry.fbresults) returnValue(None)
def __sub__(self, dateorduration): if isinstance(dateorduration, DateTime): date = dateorduration # Look for floating if self.floating() or date.floating(): # Adjust the floating ones to fixed copy1 = self.duplicate() copy2 = date.duplicate() if copy1.floating() and copy2.floating(): # Set both to UTC and do comparison copy1.setTimezoneUTC(True) copy2.setTimezoneUTC(True) return copy1 - copy2 elif copy1.floating(): # Set to be the same copy1.setTimezoneID(copy2.getTimezoneID()) copy1.setTimezoneUTC(copy2.getTimezoneUTC()) return copy1 - copy2 else: # Set to be the same copy2.setTimezoneID(copy1.getTimezoneID()) copy2.setTimezoneUTC(copy1.getTimezoneUTC()) return copy1 - copy2 else: # Do diff of date-time in seconds diff = self.getPosixTime() - date.getPosixTime() return Duration(duration=diff) elif isinstance(dateorduration, Duration): duration = dateorduration result = self.duplicate() result.mSeconds -= duration.getTotalSeconds() result.normalise() return result raise ValueError("DateTime: cannot subtract: {}".format(dateorduration))
def parse(self, data, fullISO=False): try: splits = data.split('/', 1) if len(splits) == 2: start = splits[0] end = splits[1] self.mStart.parse(start, fullISO) if end[0] == 'P': self.mDuration = Duration.parseText(end) self.mUseDuration = True self.mEnd = None else: self.mEnd.parse(end, fullISO) self.mUseDuration = False self.mDuration = None else: raise ValueError except IndexError: raise ValueError
class Period(ValueMixin): def __init__(self, start=None, end=None, duration=None): self.mStart = start if start is not None else DateTime() if end is not None: self.mEnd = end self.mDuration = self.mEnd - self.mStart self.mUseDuration = False elif duration is not None: self.mDuration = duration self.mEnd = self.mStart + self.mDuration self.mUseDuration = True else: self.mEnd = self.mStart.duplicate() self.mDuration = Duration() self.mUseDuration = False def duplicate(self): other = Period(start=self.mStart.duplicate(), end=self.mEnd.duplicate()) other.mUseDuration = self.mUseDuration return other def __hash__(self): return hash(( self.mStart, self.mEnd, )) def __repr__(self): return "Period %s" % (self.getText(), ) def __str__(self): return self.getText() def __eq__(self, comp): return self.mStart == comp.mStart and self.mEnd == comp.mEnd def __gt__(self, comp): return self.mStart > comp def __lt__(self, comp): return self.mStart < comp.mStart \ or (self.mStart == comp.mStart) and self.mEnd < comp.mEnd @classmethod def parseText(cls, data): period = cls() period.parse(data) return period def parse(self, data, fullISO=False): splits = data.split('/', 1) if len(splits) == 2: start = splits[0] end = splits[1] self.mStart.parse(start, fullISO) if end[0] == 'P': self.mDuration.parse(end) self.mUseDuration = True self.mEnd = self.mStart + self.mDuration else: self.mEnd.parse(end, fullISO) self.mUseDuration = False self.mDuration = self.mEnd - self.mStart else: raise ValueError def generate(self, os): try: self.mStart.generate(os) os.write("/") if self.mUseDuration: self.mDuration.generate(os) else: self.mEnd.generate(os) except: pass def writeXML(self, node, namespace): start = XML.SubElement( node, xmlutils.makeTag(namespace, xmldefinitions.period_start)) start.text = self.mStart.getXMLText() if self.mUseDuration: duration = XML.SubElement( node, xmlutils.makeTag(namespace, xmldefinitions.period_duration)) duration.text = self.mDuration.getText() else: end = XML.SubElement( node, xmlutils.makeTag(namespace, xmldefinitions.period_end)) end.text = self.mEnd.getXMLText() def parseJSON(self, jobject): """ jCal encodes this as an array of two values. We convert back into a single "/" separated string and parse as normal. """ self.parse("%s/%s" % tuple(jobject), True) def writeJSON(self, jobject): """ jCal encodes this value as an array with two components. """ value = [ self.mStart.getXMLText(), ] if self.mUseDuration: value.append(self.mDuration.getText()) else: value.append(self.mEnd.getXMLText()) jobject.append(value) def getStart(self): return self.mStart def getEnd(self): return self.mEnd def getDuration(self): return self.mDuration def getUseDuration(self): return self.mUseDuration def setUseDuration(self, use): self.mUseDuration = use def isDateWithinPeriod(self, dt): # Inclusive start, exclusive end return dt >= self.mStart and dt < self.mEnd def isDateBeforePeriod(self, dt): # Inclusive start return dt < self.mStart def isDateAfterPeriod(self, dt): # Exclusive end return dt >= self.mEnd def isPeriodOverlap(self, p): # Inclusive start, exclusive end return not (self.mStart >= p.mEnd or self.mEnd <= p.mStart) def adjustToUTC(self): self.mStart.adjustToUTC() self.mEnd.adjustToUTC() def describeDuration(self): return ""
def indexedSearch(self, filter, useruid="", fbtype=False): """ Finds resources matching the given qualifiers. @param filter: the L{Filter} for the calendar-query to execute. @return: an iterable of tuples for each resource matching the given C{qualifiers}. The tuples are C{(name, uid, type)}, where C{name} is the resource name, C{uid} is the resource UID, and C{type} is the resource iCalendar component type. """ # Make sure we have a proper Filter element and get the partial SQL # statement to use. if isinstance(filter, Filter): if fbtype: # Lookup the useruid - try the empty (default) one if needed dbuseruid = self._db_value_for_sql( "select PERUSERID from PERUSER where USERUID == :1", useruid, ) else: dbuseruid = "" qualifiers = sqlcalendarquery(filter, None, dbuseruid, fbtype) if qualifiers is not None: # Determine how far we need to extend the current expansion of # events. If we have an open-ended time-range we will expand one # year past the start. That should catch bounded recurrences - unbounded # will have been indexed with an "infinite" value always included. maxDate, isStartDate = filter.getmaxtimerange() if maxDate: maxDate = maxDate.duplicate() maxDate.setDateOnly(True) if isStartDate: maxDate += Duration(days=365) self.testAndUpdateIndex(maxDate) else: # We cannot handle this filter in an indexed search raise IndexedSearchException() else: qualifiers = None # Perform the search if qualifiers is None: rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE") else: if fbtype: # For a free-busy time-range query we return all instances rowiter = self._db_execute( "select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE, RESOURCE.ORGANIZER, TIMESPAN.FLOAT, TIMESPAN.START, TIMESPAN.END, TIMESPAN.FBTYPE, TIMESPAN.TRANSPARENT, TRANSPARENCY.TRANSPARENT" + qualifiers[0], *qualifiers[1]) else: rowiter = self._db_execute( "select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE" + qualifiers[0], *qualifiers[1]) # Check result for missing resources results = [] for row in rowiter: name = row[0] if self.resource.getChild(name.encode("utf-8")): if fbtype: row = list(row) if row[9]: row[8] = row[9] del row[9] results.append(row) else: log.error( "Calendar resource %s is missing from %s. Removing from index." % (name, self.resource)) self.deleteResource(name) return results
def _add_to_db(self, name, calendar, cursor=None, expand_until=None, reCreate=False): """ Records the given calendar resource in the index with the given name. Resource names and UIDs must both be unique; only one resource name may be associated with any given UID and vice versa. NB This method does not commit the changes to the db - the caller MUST take care of that @param name: the name of the resource to add. @param calendar: a L{Calendar} object representing the resource contents. """ uid = calendar.resourceUID() organizer = calendar.getOrganizer() if not organizer: organizer = "" # Decide how far to expand based on the component doInstanceIndexing = False master = calendar.masterComponent() if master is None or not calendar.isRecurring(): # When there is no master we have a set of overridden components - index them all. # When there is one instance - index it. expand = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) doInstanceIndexing = True else: # If migrating or re-creating or config option for delayed indexing is off, always index if reCreate or not config.FreeBusyIndexDelayedExpand: doInstanceIndexing = True # Duration into the future through which recurrences are expanded in the index # by default. This is a caching parameter which affects the size of the index; # it does not affect search results beyond this period, but it may affect # performance of such a search. expand = (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandAheadDays)) if expand_until and expand_until > expand: expand = expand_until # Maximum duration into the future through which recurrences are expanded in the # index. This is a caching parameter which affects the size of the index; it # does not affect search results beyond this period, but it may affect # performance of such a search. # # When a search is performed on a time span that goes beyond that which is # expanded in the index, we have to open each resource which may have data in # that time period. In order to avoid doing that multiple times, we want to # cache those results. However, we don't necessarily want to cache all # occurrences into some obscenely far-in-the-future date, so we cap the caching # period. Searches beyond this period will always be relatively expensive for # resources with occurrences beyond this period. if expand > (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandMaxDays)): raise IndexedSearchException() # Always do recurrence expansion even if we do not intend to index - we need this to double-check the # validity of the iCalendar recurrence data. try: instances = calendar.expandTimeRanges( expand, ignoreInvalidInstances=reCreate) recurrenceLimit = instances.limit except InvalidOverriddenInstanceError, e: log.error("Invalid instance %s when indexing %s in %s" % ( e.rid, name, self.resource, )) raise
def doWork(self): # Delete all other work items for this event yield Delete( From=self.table, Where=self.group, ).on(self.transaction) # get db object calendarObject = yield CalendarStoreFeatures( self.transaction._store).calendarObjectWithID( self.transaction, self.resourceID) component = yield calendarObject.componentForUser() # Change a copy of the original, as we need the original cached on the resource # so we can do a diff to test implicit scheduling changes component = component.duplicate() # sync group attendees if (yield calendarObject.reconcileGroupAttendees(component)): # group attendees in event have changed if (component.masterComponent() is None or not component.isRecurring()): # skip non-recurring old events, no instances if (yield calendarObject.removeOldEventGroupLink( component, instances=None, inserting=False, txn=self.transaction)): returnValue(None) else: # skip recurring old events expand = (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandAheadDays)) if config.FreeBusyIndexLowerLimitDays: truncateLowerLimit = DateTime.getToday() truncateLowerLimit.offsetDay( -config.FreeBusyIndexLowerLimitDays) else: truncateLowerLimit = None instances = component.expandTimeRanges( expand, lowerLimit=truncateLowerLimit, ignoreInvalidInstances=True) if (yield calendarObject.removeOldEventGroupLink( component, instances=instances, inserting=False, txn=self.transaction)): returnValue(None) # split spanning events and only update present-future split result splitter = iCalSplitter(0, 1) break_point = DateTime.getToday() - Duration( seconds=config.GroupAttendees.UpdateOldEventLimitSeconds) rid = splitter.whereSplit(component, break_point=break_point) if rid is not None: yield calendarObject.split(onlyThis=True, rid=rid) # remove group link to ensure update (update to unknown hash would work too) # FIXME: its possible that more than one group id gets updated during this single work item, so we # need to make sure that ALL the group_id's are removed by this query. ga = schema.GROUP_ATTENDEE yield Delete(From=ga, Where=(ga.RESOURCE_ID == self.resourceID).And( ga.GROUP_ID == self.groupID)).on( self.transaction) # update group attendee in remaining component component = yield calendarObject.componentForUser() component = component.duplicate() change = yield calendarObject.reconcileGroupAttendees( component) assert change yield calendarObject._setComponentInternal( component, False, ComponentUpdateState.SPLIT_OWNER) returnValue(None) yield calendarObject.setComponent(component)
def _matchCalendarResources(self, calresource): # Get the timezone property from the collection. tz = calresource.getTimezone() # Try cache aggregated_resources = (yield FBCacheEntry.getCacheEntry(calresource, self.attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None if aggregated_resources is None: if self.accountingItems is not None: self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if self.logItems is not None: self.logItems["fb-uncached"] = self.logItems.get("fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward)) # If the requested time range would fit in our allowed cache range, trigger the cache creation if compareDateTime(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0: cache_timerange = Period(cache_start, cache_end) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range tr = TimeRange( start=(cache_timerange if caching else self.timerange).getStart().getText(), end=(cache_timerange if caching else self.timerange).getEnd().getText(), ) filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( tr, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", ) ) filter = Filter(filter) tzinfo = filter.settimezone(tz) if self.accountingItems is not None: self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),) try: resources = yield calresource.search(filter, useruid=self.attendee_uid, fbtype=True) aggregated_resources = {} for name, uid, comptype, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault((name, uid, comptype, test_organizer,), []).append(( float, tupleFromDateTime(parseSQLTimestampToPyCalendar(start)), tupleFromDateTime(parseSQLTimestampToPyCalendar(end)), fbtype, )) if caching: yield FBCacheEntry.makeCacheEntry(calresource, self.attendee_uid, cache_timerange, aggregated_resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if self.accountingItems is not None: self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1 # Log extended item if self.logItems is not None: self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone.UTCTimezone filter = None returnValue((aggregated_resources, tzinfo, filter,))
def testParse(self): for seconds, result in TestDuration.test_data: duration = Duration().parseText(result) self.assertEqual(duration.getTotalSeconds(), seconds)
class VAlarm(Component): sActionMap = { definitions.cICalProperty_ACTION_AUDIO: definitions.eAction_VAlarm_Audio, definitions.cICalProperty_ACTION_DISPLAY: definitions.eAction_VAlarm_Display, definitions.cICalProperty_ACTION_EMAIL: definitions.eAction_VAlarm_Email, definitions.cICalProperty_ACTION_PROCEDURE: definitions.eAction_VAlarm_Procedure, definitions.cICalProperty_ACTION_URI: definitions.eAction_VAlarm_URI, definitions.cICalProperty_ACTION_NONE: definitions.eAction_VAlarm_None, } sActionValueMap = { definitions.eAction_VAlarm_Audio: definitions.cICalProperty_ACTION_AUDIO, definitions.eAction_VAlarm_Display: definitions.cICalProperty_ACTION_DISPLAY, definitions.eAction_VAlarm_Email: definitions.cICalProperty_ACTION_EMAIL, definitions.eAction_VAlarm_Procedure: definitions.cICalProperty_ACTION_PROCEDURE, definitions.eAction_VAlarm_URI: definitions.cICalProperty_ACTION_URI, definitions.eAction_VAlarm_None: definitions.cICalProperty_ACTION_NONE, } # Classes for each action encapsulating action-specific data class VAlarmAction(object): propertyCardinality_1 = () propertyCardinality_1_Fix_Empty = () propertyCardinality_0_1 = () propertyCardinality_1_More = () def __init__(self, type): self.mType = type def duplicate(self): return VAlarm.VAlarmAction(self.mType) def load(self, valarm): pass def add(self, valarm): pass def remove(self, valarm): pass def getType(self): return self.mType class VAlarmAudio(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ATTACH, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self, speak=None): super(VAlarm.VAlarmAudio, self).__init__(type=definitions.eAction_VAlarm_Audio) self.mSpeakText = speak def duplicate(self): return VAlarm.VAlarmAudio(self.mSpeakText) def load(self, valarm): # Get properties self.mSpeakText = valarm.loadValueString(definitions.cICalProperty_ACTION_X_SPEAKTEXT) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_ACTION_X_SPEAKTEXT, self.mSpeakText) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties(definitions.cICalProperty_ACTION_X_SPEAKTEXT) def isSpeakText(self): return len(self.mSpeakText) != 0 def getSpeakText(self): return self.mSpeakText class VAlarmDisplay(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_1_Fix_Empty = ( definitions.cICalProperty_DESCRIPTION, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self, description=None): super(VAlarm.VAlarmDisplay, self).__init__(type=definitions.eAction_VAlarm_Display) self.mDescription = description def duplicate(self): return VAlarm.VAlarmDisplay(self.mDescription) def load(self, valarm): # Get properties self.mDescription = valarm.loadValueString(definitions.cICalProperty_DESCRIPTION) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_DESCRIPTION, self.mDescription) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties(definitions.cICalProperty_DESCRIPTION) def getDescription(self): return self.mDescription class VAlarmEmail(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_1_Fix_Empty = ( definitions.cICalProperty_DESCRIPTION, definitions.cICalProperty_SUMMARY, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) propertyCardinality_1_More = ( definitions.cICalProperty_ATTENDEE, ) def __init__(self, description=None, summary=None, attendees=None): super(VAlarm.VAlarmEmail, self).__init__(type=definitions.eAction_VAlarm_Email) self.mDescription = description self.mSummary = summary self.mAttendees = attendees def duplicate(self): return VAlarm.VAlarmEmail(self.mDescription, self.mSummary, self.mAttendees) def load(self, valarm): # Get properties self.mDescription = valarm.loadValueString(definitions.cICalProperty_DESCRIPTION) self.mSummary = valarm.loadValueString(definitions.cICalProperty_SUMMARY) self.mAttendees = [] if valarm.hasProperty(definitions.cICalProperty_ATTENDEE): # Get each attendee range = valarm.getProperties().get(definitions.cICalProperty_ATTENDEE, ()) for iter in range: # Get the attendee value attendee = iter.getCalAddressValue() if attendee is not None: self.mAttendees.append(attendee.getValue()) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_DESCRIPTION, self.mDescription) valarm.addProperty(prop) prop = Property(definitions.cICalProperty_SUMMARY, self.mSummary) valarm.addProperty(prop) for iter in self.mAttendees: prop = Property(definitions.cICalProperty_ATTENDEE, iter, Value.VALUETYPE_CALADDRESS) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties(definitions.cICalProperty_DESCRIPTION) valarm.removeProperties(definitions.cICalProperty_SUMMARY) valarm.removeProperties(definitions.cICalProperty_ATTENDEE) def getDescription(self): return self.mDescription def getSummary(self): return self.mSummary def getAttendees(self): return self.mAttendees class VAlarmUnknown(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self): super(VAlarm.VAlarmUnknown, self).__init__(type=definitions.eAction_VAlarm_Unknown) def duplicate(self): return VAlarm.VAlarmUnknown() class VAlarmURI(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, definitions.cICalProperty_URL, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self, uri=None): super(VAlarm.VAlarmURI, self).__init__(type=definitions.eAction_VAlarm_URI) self.mURI = uri def duplicate(self): return VAlarm.VAlarmURI(self.mURI) def load(self, valarm): # Get properties self.mURI = valarm.loadValueString(definitions.cICalProperty_URL) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_URL, self.mURI) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties(definitions.cICalProperty_URL) def getURI(self): return self.mURI class VAlarmNone(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, ) def __init__(self): super(VAlarm.VAlarmNone, self).__init__(type=definitions.eAction_VAlarm_None) def duplicate(self): return VAlarm.VAlarmNone() def getMimeComponentName(self): # Cannot be sent as a separate MIME object return None sActionToAlarmMap = { definitions.eAction_VAlarm_Audio: VAlarmAudio, definitions.eAction_VAlarm_Display: VAlarmDisplay, definitions.eAction_VAlarm_Email: VAlarmEmail, definitions.eAction_VAlarm_URI: VAlarmURI, definitions.eAction_VAlarm_None: VAlarmNone, } propertyValueChecks = ICALENDAR_VALUE_CHECKS def __init__(self, parent=None): super(VAlarm, self).__init__(parent=parent) self.mAction = definitions.eAction_VAlarm_Display self.mTriggerAbsolute = False self.mTriggerOnStart = True self.mTriggerOn = DateTime() # Set duration default to 1 hour self.mTriggerBy = Duration() self.mTriggerBy.setDuration(60 * 60) # Does not repeat by default self.mRepeats = 0 self.mRepeatInterval = Duration() self.mRepeatInterval.setDuration(5 * 60) # Five minutes # Status self.mStatusInit = False self.mAlarmStatus = definitions.eAlarm_Status_Pending self.mLastTrigger = DateTime() self.mNextTrigger = DateTime() self.mDoneCount = 0 # Create action data self.mActionData = VAlarm.VAlarmDisplay("") def duplicate(self, parent=None): other = super(VAlarm, self).duplicate(parent=parent) other.mAction = self.mAction other.mTriggerAbsolute = self.mTriggerAbsolute other.mTriggerOn = self.mTriggerOn.duplicate() other.mTriggerBy = self.mTriggerBy.duplicate() other.mTriggerOnStart = self.mTriggerOnStart other.mRepeats = self.mRepeats other.mRepeatInterval = self.mRepeatInterval.duplicate() other.mAlarmStatus = self.mAlarmStatus if self.mLastTrigger is not None: other.mLastTrigger = self.mLastTrigger.duplicate() if self.mNextTrigger is not None: other.mNextTrigger = self.mNextTrigger.duplicate() other.mDoneCount = self.mDoneCount other.mActionData = self.mActionData.duplicate() return other def getType(self): return definitions.cICalComponent_VALARM def getAction(self): return self.mAction def getActionData(self): return self.mActionData def isTriggerAbsolute(self): return self.mTriggerAbsolute def getTriggerOn(self): return self.mTriggerOn def getTriggerDuration(self): return self.mTriggerBy def isTriggerOnStart(self): return self.mTriggerOnStart def getRepeats(self): return self.mRepeats def getInterval(self): return self.mRepeatInterval def added(self): # Added to calendar so add to calendar notifier # calstore::CCalendarNotifier::sCalendarNotifier.AddAlarm(this) # Do inherited super(VAlarm, self).added() def removed(self): # Removed from calendar so add to calendar notifier # calstore::CCalendarNotifier::sCalendarNotifier.RemoveAlarm(this) # Do inherited super(VAlarm, self).removed() def changed(self): # Always force recalc of trigger status self.mStatusInit = False # Changed in calendar so change in calendar notifier # calstore::CCalendarNotifier::sCalendarNotifier.ChangedAlarm(this) # Do not do inherited as this is always a sub-component and we do not # do top-level component changes # super.changed() def finalise(self): # Do inherited super(VAlarm, self).finalise() # Get the ACTION temp = self.loadValueString(definitions.cICalProperty_ACTION) if temp is not None: self.mAction = VAlarm.sActionMap.get(temp, definitions.eAction_VAlarm_Unknown) self.loadAction() # Get the trigger if self.hasProperty(definitions.cICalProperty_TRIGGER): # Determine the type of the value temp1 = self.loadValueDateTime(definitions.cICalProperty_TRIGGER) temp2 = self.loadValueDuration(definitions.cICalProperty_TRIGGER) if temp1 is not None: self.mTriggerAbsolute = True self.mTriggerOn = temp1 elif temp2 is not None: self.mTriggerAbsolute = False self.mTriggerBy = temp2 # Get the property prop = self.findFirstProperty(definitions.cICalProperty_TRIGGER) # Look for RELATED parameter if prop.hasParameter(definitions.cICalParameter_RELATED): temp = prop.getParameterValue(definitions.cICalParameter_RELATED) if temp == definitions.cICalParameter_RELATED_START: self.mTriggerOnStart = True elif temp == definitions.cICalParameter_RELATED_END: self.mTriggerOnStart = False else: self.mTriggerOnStart = True # Get repeat & interval temp = self.loadValueInteger(definitions.cICalProperty_REPEAT) if temp is not None: self.mRepeats = temp temp = self.loadValueDuration(definitions.cICalProperty_DURATION) if temp is not None: self.mRepeatInterval = temp # Set a map key for sorting self.mMapKey = "%s:%s" % (self.mAction, self.mTriggerOn if self.mTriggerAbsolute else self.mTriggerBy,) # Alarm status - private to Mulberry status = self.loadValueString(definitions.cICalProperty_ALARM_X_ALARMSTATUS) if status is not None: if status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING: self.mAlarmStatus = definitions.eAlarm_Status_Pending elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED: self.mAlarmStatus = definitions.eAlarm_Status_Completed elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED: self.mAlarmStatus = definitions.eAlarm_Status_Disabled else: self.mAlarmStatus = definitions.eAlarm_Status_Pending # Last trigger time - private to Mulberry temp = self.loadValueDateTime(definitions.cICalProperty_ALARM_X_LASTTRIGGER) if temp is not None: self.mLastTrigger = temp def validate(self, doFix=False): """ Validate the data in this component and optionally fix any problems, else raise. If loggedProblems is not None it must be a C{list} and problem descriptions are appended to that. """ # Validate using action specific constraints self.propertyCardinality_1 = self.mActionData.propertyCardinality_1 self.propertyCardinality_1_Fix_Empty = self.mActionData.propertyCardinality_1_Fix_Empty self.propertyCardinality_0_1 = self.mActionData.propertyCardinality_0_1 self.propertyCardinality_1_More = self.mActionData.propertyCardinality_1_More fixed, unfixed = super(VAlarm, self).validate(doFix) # Extra constraint: both DURATION and REPEAT must be present togethe if self.hasProperty(definitions.cICalProperty_DURATION) ^ self.hasProperty(definitions.cICalProperty_REPEAT): # Cannot fix this logProblem = "[%s] Properties must be present together: %s, %s" % ( self.getType(), definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, ) unfixed.append(logProblem) return fixed, unfixed def editStatus(self, status): # Remove existing self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS) # Updated cached values self.mAlarmStatus = status # Add new status_txt = "" if self.mAlarmStatus == definitions.eAlarm_Status_Pending: status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING elif self.mAlarmStatus == definitions.eAlarm_Status_Completed: status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled: status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED self.addProperty(Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status_txt)) def editAction(self, action, data): # Remove existing self.removeProperties(definitions.cICalProperty_ACTION) self.mActionData.remove(self) self.mActionData = None # Updated cached values self.mAction = action self.mActionData = data # Add new properties to alarm action_txt = VAlarm.sActionValueMap.get(self.mAction, definitions.cICalProperty_ACTION_PROCEDURE) prop = Property(definitions.cICalProperty_ACTION, action_txt) self.addProperty(prop) self.mActionData.add(self) def editTriggerOn(self, dt): # Remove existing self.removeProperties(definitions.cICalProperty_TRIGGER) # Updated cached values self.mTriggerAbsolute = True self.mTriggerOn = dt # Add new prop = Property(definitions.cICalProperty_TRIGGER, dt) self.addProperty(prop) def editTriggerBy(self, duration, trigger_start): # Remove existing self.removeProperties(definitions.cICalProperty_TRIGGER) # Updated cached values self.mTriggerAbsolute = False self.mTriggerBy = duration self.mTriggerOnStart = trigger_start # Add new (with parameter) prop = Property(definitions.cICalProperty_TRIGGER, duration) attr = Parameter( definitions.cICalParameter_RELATED, ( definitions.cICalParameter_RELATED_START, definitions.cICalParameter_RELATED_END )[not trigger_start] ) prop.addParameter(attr) self.addProperty(prop) def editRepeats(self, repeat, interval): # Remove existing self.removeProperties(definitions.cICalProperty_REPEAT) self.removeProperties(definitions.cICalProperty_DURATION) # Updated cached values self.mRepeats = repeat self.mRepeatInterval = interval # Add new if self.mRepeats > 0: self.addProperty(Property(definitions.cICalProperty_REPEAT, repeat)) self.addProperty(Property(definitions.cICalProperty_DURATION, interval)) def getAlarmStatus(self): return self.mAlarmStatus def getNextTrigger(self, dt): if not self.mStatusInit: self.initNextTrigger() dt.copy(self.mNextTrigger) def alarmTriggered(self, dt): # Remove existing self.removeProperties(definitions.cICalProperty_ALARM_X_LASTTRIGGER) self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS) # Updated cached values self.mLastTrigger.copy(dt) if self.mDoneCount < self.mRepeats: self.mNextTrigger = self.mLastTrigger + self.mRepeatInterval dt.copy(self.mNextTrigger) self.mDoneCount += 1 self.mAlarmStatus = definitions.eAlarm_Status_Pending else: self.mAlarmStatus = definitions.eAlarm_Status_Completed # Add new self.addProperty(Property(definitions.cICalProperty_ALARM_X_LASTTRIGGER, dt)) status = "" if self.mAlarmStatus == definitions.eAlarm_Status_Pending: status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING elif self.mAlarmStatus == definitions.eAlarm_Status_Completed: status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled: status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED self.addProperty(Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status)) # Now update dt to the next alarm time return self.mAlarmStatus == definitions.eAlarm_Status_Pending def loadAction(self): # Delete current one self.mActionData = None self.mActionData = VAlarm.sActionToAlarmMap.get(self.mAction, VAlarm.VAlarmUnknown)() self.mActionData.load(self) def initNextTrigger(self): # Do not bother if its completed if self.mAlarmStatus == definitions.eAlarm_Status_Completed: return self.mStatusInit = True # Look for trigger immediately preceeding or equal to utc now nowutc = DateTime.getNowUTC() # Init done counter self.mDoneCount = 0 # Determine the first trigger trigger = DateTime() self.getFirstTrigger(trigger) while self.mDoneCount < self.mRepeats: # See if next trigger is later than now next_trigger = trigger + self.mRepeatInterval if next_trigger > nowutc: break self.mDoneCount += 1 trigger = next_trigger # Check for completion if trigger == self.mLastTrigger or (nowutc - trigger).getTotalSeconds() > 24 * 60 * 60: if self.mDoneCount == self.mRepeats: self.mAlarmStatus = definitions.eAlarm_Status_Completed return else: trigger = trigger + self.mRepeatInterval self.mDoneCount += 1 self.mNextTrigger = trigger def getFirstTrigger(self, dt): # If absolute trigger, use that if self.isTriggerAbsolute(): # Get the trigger on dt.copy(self.getTriggerOn()) else: # Get the parent embedder class (must be CICalendarComponentRecur type) owner = self.getEmbedder() if owner is not None: # Determine time at which alarm will trigger trigger = (owner.getStart(), owner.getEnd())[not self.isTriggerOnStart()] # Offset by duration dt.copy(trigger + self.getTriggerDuration())
def outbound(self, txn, originator, recipient, calendar, onlyAfter=None): """ Generates and sends an outbound IMIP message. @param txn: the transaction to use for looking up/creating tokens @type txn: L{CommonStoreTransaction} """ if onlyAfter is None: duration = Duration(days=self.suppressionDays) onlyAfter = DateTime.getNowUTC() - duration icaluid = calendar.resourceUID() method = calendar.propertyValue("METHOD") # Clean up the attendee list which is purely used within the human # readable email message (not modifying the calendar body) attendees = [] for attendeeProp in calendar.getAllAttendeeProperties(): cutype = attendeeProp.parameterValue("CUTYPE", "INDIVIDUAL") if cutype == "INDIVIDUAL": cn = attendeeProp.parameterValue("CN", None) if cn is not None: cn = cn.decode("utf-8") cuaddr = normalizeCUAddr(attendeeProp.value()) if cuaddr.startswith("mailto:"): mailto = cuaddr[7:] if not cn: cn = mailto else: emailAddress = attendeeProp.parameterValue("EMAIL", None) if emailAddress: mailto = emailAddress else: mailto = None if cn or mailto: attendees.append((cn, mailto)) toAddr = recipient if not recipient.lower().startswith("mailto:"): raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP " "operation." % (recipient, )) recipient = recipient[7:] if method != "REPLY": # Invites and cancellations: # Reuse or generate a token based on originator, toAddr, and # event uid token = (yield txn.imipGetToken(originator, toAddr.lower(), icaluid)) if token is None: # Because in the past the originator was sometimes in mailto: # form, lookup an existing token by mailto: as well organizerProperty = calendar.getOrganizerProperty() organizerEmailAddress = organizerProperty.parameterValue( "EMAIL", None) if organizerEmailAddress is not None: token = (yield txn.imipGetToken( "mailto:%s" % (organizerEmailAddress.lower(), ), toAddr.lower(), icaluid)) if token is None: token = (yield txn.imipCreateToken(originator, toAddr.lower(), icaluid)) self.log.debug( "Mail gateway created token %s for %s " "(originator), %s (recipient) and %s (icaluid)" % (token, originator, toAddr, icaluid)) inviteState = "new" else: self.log.debug( "Mail gateway reusing token %s for %s " "(originator), %s (recipient) and %s (icaluid)" % (token, originator, toAddr, icaluid)) inviteState = "update" fullServerAddress = self.address _ignore_name, serverAddress = email.utils.parseaddr( fullServerAddress) pre, post = serverAddress.split('@') addressWithToken = "%s+%s@%s" % (pre, token, post) organizerProperty = calendar.getOrganizerProperty() organizerEmailAddress = organizerProperty.parameterValue( "EMAIL", None) organizerValue = organizerProperty.value() organizerProperty.setValue("mailto:%s" % (addressWithToken, )) # If the organizer is also an attendee, update that attendee value # to match organizerAttendeeProperty = calendar.getAttendeeProperty( [organizerValue]) if organizerAttendeeProperty is not None: organizerAttendeeProperty.setValue("mailto:%s" % (addressWithToken, )) # The email's From will include the originator's real name email # address if available. Otherwise it will be the server's email # address (without # + addressing) if organizerEmailAddress: orgEmail = fromAddr = organizerEmailAddress else: fromAddr = serverAddress orgEmail = None cn = calendar.getOrganizerProperty().parameterValue('CN', None) if cn is None: cn = u'Calendar Server' orgCN = orgEmail else: orgCN = cn = cn.decode("utf-8") # a unicode cn (rather than an encode string value) means the # from address will get properly encoded per rfc2047 within the # MIMEMultipart in generateEmail formattedFrom = "%s <%s>" % (cn, fromAddr) # Reply-to address will be the server+token address else: # REPLY inviteState = "reply" # Look up the attendee property corresponding to the originator # of this reply originatorAttendeeProperty = calendar.getAttendeeProperty( [originator]) formattedFrom = fromAddr = originator = "" if originatorAttendeeProperty: originatorAttendeeEmailAddress = ( originatorAttendeeProperty.parameterValue("EMAIL", None)) if originatorAttendeeEmailAddress: formattedFrom = fromAddr = originator = ( originatorAttendeeEmailAddress) organizerMailto = str(calendar.getOrganizer()) if not organizerMailto.lower().startswith("mailto:"): raise ValueError("ORGANIZER address '%s' must be mailto: " "for REPLY." % (organizerMailto, )) orgEmail = organizerMailto[7:] orgCN = calendar.getOrganizerProperty().parameterValue('CN', None) addressWithToken = formattedFrom # At the point we've created the token in the db, which we always # want to do, but if this message is for an event completely in # the past we don't want to actually send an email. if not calendar.hasInstancesAfter(onlyAfter): self.log.debug("Skipping IMIP message for old event") returnValue(True) # Now prevent any "internal" CUAs from being exposed by converting # to mailto: if we have one for attendeeProp in calendar.getAllAttendeeProperties(): cutype = attendeeProp.parameterValue('CUTYPE', None) if cutype == "INDIVIDUAL": cuaddr = normalizeCUAddr(attendeeProp.value()) if not cuaddr.startswith("mailto:"): emailAddress = attendeeProp.parameterValue("EMAIL", None) if emailAddress: attendeeProp.setValue("mailto:%s" % (emailAddress, )) msgId, message = self.generateEmail(inviteState, calendar, orgEmail, orgCN, attendees, formattedFrom, addressWithToken, recipient, language=self.language) try: success = (yield self.smtpSender.sendMessage(fromAddr, toAddr, msgId, message)) returnValue(success) except Exception, e: self.log.error("Failed to send IMIP message (%s)" % (str(e), )) returnValue(False)
def _processFBURL(self, request): # # Check authentication and access controls # yield self.authorize(request, (davxml.Read(),)) # Extract query parameters from the URL args = ('start', 'end', 'duration', 'token', 'format', 'user',) for arg in args: setattr(self, arg, request.args.get(arg, [None])[0]) # Some things we do not handle if self.token or self.user: raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-query-parameter"), "Invalid query parameter", )) # Check format if self.format: self.format = self.format.split(";")[0] if self.format not in ("text/calendar", "text/plain"): raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-format"), "Invalid return format requested", )) else: self.format = "text/calendar" # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values try: if self.start: self.start = DateTime.parseText(self.start) if not self.start.utc(): raise ValueError() if self.end: self.end = DateTime.parseText(self.end) if not self.end.utc(): raise ValueError() if self.duration: self.duration = Duration.parseText(self.duration) except ValueError: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Sanity check start/end/duration # End and duration cannot both be present if self.end and self.duration: raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Duration must be positive if self.duration and self.duration.getTotalSeconds() < 0: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Now fill in the missing pieces if self.start is None: self.start = DateTime.getNowUTC() self.start.setHHMMSS(0, 0, 0) if self.duration: self.end = self.start + self.duration if self.end is None: self.end = self.start + Duration(days=config.FreeBusyURL.TimePeriod) # End > start if self.end <= self.start: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long) # Now lookup the principal details for the targeted user principal = (yield self.parent.principalForRecord()) # Pick the first mailto cu address or the first other type cuaddr = None for item in principal.calendarUserAddresses(): if cuaddr is None: cuaddr = item if item.startswith("mailto:"): cuaddr = item break # Get inbox details inboxURL = principal.scheduleInboxURL() if inboxURL is None: raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,))) try: inbox = (yield request.locateResource(inboxURL)) except: log.error("No schedule inbox for principal: {p}", p=principal) inbox = None if inbox is None: raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,))) organizer = recipient = LocalCalendarUser(cuaddr, principal.record) recipient.inbox = inbox._newStoreObject attendeeProp = Property("ATTENDEE", recipient.cuaddr) timerange = Period(self.start, self.end) fbresult = yield FreebusyQuery( organizer=organizer, recipient=recipient, attendeeProp=attendeeProp, timerange=timerange, ).generateAttendeeFreeBusyResponse() response = Response() response.stream = MemoryStream(str(fbresult)) response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,))) returnValue(response)
def _internalGenerateFreeBusyInfo( calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, logItems=None, accountingItems=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{Calendar} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} @param logItems: a C{dict} to store logging info to @param accountingItems: a C{dict} to store accounting info to """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy # May need organizer principal organizer_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress(organizer)) if organizer else None organizer_uid = organizer_record.uid if organizer_record else "" # Free busy is per-user attendee_uid = calresource.viewerHome().uid() attendee_record = yield calresource.directoryService().recordWithUID( attendee_uid.decode("utf-8")) # Get the timezone property from the collection. tz = calresource.getTimezone() # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_record is not None and attendee_record is not None: # Get the principal of the authorized user which may be different from the organizer if a delegate of # the organizer is making the request authz_uid = organizer_uid authz_record = organizer_record if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid: authz_uid = calresource._txn._authz_uid authz_record = yield calresource.directoryService().recordWithUID( authz_uid.decode("utf-8")) # Check if attendee is also the organizer or the delegate doing the request if attendee_uid in (organizer_uid, authz_uid): do_event_details = True rich_options["organizer"] = True # Check if authorized user is a delegate of attendee proxy = (yield authz_record.isProxyFor(attendee_record)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType( ) in ( "RESOURCE", "ROOM", ): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry( calresource, attendee_uid, timerange)) if config.EnableFreeBusyCache else None if resources is None: if accountingItems is not None: accountingItems["fb-uncached"] = accountingItems.get( "fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if logItems is not None: logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration( days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration( days=config.FreeBusyCacheDaysForward)) # If the requested time range would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime( timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", )) filter = Filter(filter) tzinfo = filter.settimezone(tz) if accountingItems is not None: tr = cache_timerange if caching else timerange accountingItems["fb-query-timerange"] = ( str(tr.start), str(tr.end), ) try: resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if accountingItems is not None: accountingItems["fb-cached"] = accountingItems.get("fb-cached", 0) + 1 # Log extended item if logItems is not None: logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault(( name, uid, type, test_organizer, ), []).append(( float, start, end, fbtype, )) if accountingItems is not None: accountingItems["fb-resources"] = {} for k, v in aggregated_resources.items(): name, uid, type, test_organizer = k accountingItems["fb-resources"][uid] = [] for float, start, end, fbtype in v: fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) accountingItems["fb-resources"][uid].append(( float, str(fbstart), str(fbend), fbtype, )) # Cache directory record lookup outside this loop as it is expensive and will likely # always end up being called with the same organizer address. recordUIDCache = {} for key in aggregated_resources.iterkeys(): name, uid, type, test_organizer = key # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): if test_organizer: test_uid = recordUIDCache.get(test_organizer) if test_uid is None: test_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress( test_organizer)) test_uid = test_record.uid if test_record else "" recordUIDCache[test_organizer] = test_uid else: test_uid = "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_uid == "") and same_calendar_user: continue # Apply a timezone to any floating times fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) # Add extended details if do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield calresource.calendarObjectWithName(name)) calendar = (yield child.componentForUser()) # The calendar may come back as None if the resource is being changed, or was deleted # between our initial index query and getting here. For now we will ignore this error, but in # the longer term we need to implement some form of locking, perhaps. if calendar is None: log.error( "Calendar %s is missing from calendar collection %r" % (name, calresource)) continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_organizer = calendar.getOrganizer() if test_organizer: test_uid = recordUIDCache.get(test_organizer) if test_uid is None: test_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress(test_organizer)) test_uid = test_record.uid if test_record else "" recordUIDCache[test_organizer] = test_uid else: test_uid = "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_organizer is None) and same_calendar_user: continue if accountingItems is not None: accountingItems.setdefault("fb-filter-match", []).append(uid) if filter.match(calendar, None): if accountingItems is not None: accountingItems.setdefault("fb-filter-matched", []).append(uid) # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) if calendar.mainType() == "VEVENT": processEventFreeBusy(calendar, fbinfo, timerange, tzinfo) elif calendar.mainType() == "VFREEBUSY": processFreeBusyFreeBusy(calendar, fbinfo, timerange) elif calendar.mainType() == "VAVAILABILITY": processAvailabilityFreeBusy(calendar, fbinfo, timerange) else: assert "Free-busy query returned unwanted component: %s in %r", ( name, calresource, ) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)
def _processFBURL(self, request): # # Check authentication and access controls # yield self.authorize(request, (davxml.Read(),)) # Extract query parameters from the URL args = ('start', 'end', 'duration', 'token', 'format', 'user',) for arg in args: setattr(self, arg, request.args.get(arg, [None])[0]) # Some things we do not handle if self.token or self.user: raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-query-parameter"), "Invalid query parameter", )) # Check format if self.format: self.format = self.format.split(";")[0] if self.format not in ("text/calendar", "text/plain"): raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-format"), "Invalid return format requested", )) else: self.format = "text/calendar" # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values try: if self.start: self.start = DateTime.parseText(self.start) if not self.start.utc(): raise ValueError() if self.end: self.end = DateTime.parseText(self.end) if not self.end.utc(): raise ValueError() if self.duration: self.duration = Duration.parseText(self.duration) except ValueError: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Sanity check start/end/duration # End and duration cannot both be present if self.end and self.duration: raise HTTPError(ErrorResponse( responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Duration must be positive if self.duration and self.duration.getTotalSeconds() < 0: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # Now fill in the missing pieces if self.start is None: self.start = DateTime.getNowUTC() self.start.setHHMMSS(0, 0, 0) if self.duration: self.end = self.start + self.duration if self.end is None: self.end = self.start + Duration(days=config.FreeBusyURL.TimePeriod) # End > start if self.end <= self.start: raise HTTPError(ErrorResponse( responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters"), "Invalid query parameters", )) # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long) # Now lookup the principal details for the targeted user principal = (yield self.parent.principalForRecord()) # Pick the first mailto cu address or the first other type cuaddr = None for item in principal.calendarUserAddresses(): if cuaddr is None: cuaddr = item if item.startswith("mailto:"): cuaddr = item break # Get inbox details inboxURL = principal.scheduleInboxURL() if inboxURL is None: raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,))) try: inbox = (yield request.locateResource(inboxURL)) except: log.error("No schedule inbox for principal: %s" % (principal,)) inbox = None if inbox is None: raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,))) organizer = recipient = LocalCalendarUser(cuaddr, principal.record) recipient.inbox = inbox._newStoreObject attendeeProp = Property("ATTENDEE", recipient.cuaddr) timerange = Period(self.start, self.end) fbresult = yield FreebusyQuery( organizer=organizer, recipient=recipient, attendeeProp=attendeeProp, timerange=timerange, ).generateAttendeeFreeBusyResponse() response = Response() response.stream = MemoryStream(str(fbresult)) response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,))) returnValue(response)
def generateFreeBusyInfo( request, calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param request: the L{IRequest} for the current request. @param calresource: the L{CalDAVResource} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. if not servertoserver: try: yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(), ), principal=organizerPrincipal) except AccessDeniedError: returnValue(matchtotal) # May need organizer principal organizer_principal = (yield calresource.principalForCalendarUserAddress( organizer)) if organizer else None organizer_uid = organizer_principal.principalUID( ) if organizer_principal else "" # Free busy is per-user userPrincipal = (yield calresource.resourceOwnerPrincipal(request)) if userPrincipal: useruid = userPrincipal.principalUID() else: useruid = "" # Get the timezone property from the collection. has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request)) if has_prop: tz = (yield calresource.readProperty(CalendarTimeZone(), request)) else: tz = None # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_principal is not None and userPrincipal is not None: # Check if organizer is attendee if organizer_principal == userPrincipal: do_event_details = True rich_options["organizer"] = True # Check if organizer is a delegate of attendee proxy = (yield organizer_principal.isProxyFor(userPrincipal)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType( ) in ( "RESOURCE", "ROOM", ): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry( calresource, useruid, timerange)) if config.EnableFreeBusyCache else None if resources is None: caching = False if config.EnableFreeBusyCache: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems[ "fb-uncached"] = request.extendedLogItems.get( "fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration( days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration( days=config.FreeBusyCacheDaysForward)) # If the requested timerange would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime( timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", )) filter = Filter(filter) tzinfo = filter.settimezone(tz) try: resources = yield calresource.search(filter, useruid=useruid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources) except IndexedSearchException: raise HTTPError( StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "Failed freebusy query")) else: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["fb-cached"] = request.extendedLogItems.get( "fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault(( name, uid, type, test_organizer, ), []).append(( float, start, end, fbtype, )) for key in aggregated_resources.iterkeys(): name, uid, type, test_organizer = key # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_principal = ( yield calresource.principalForCalendarUserAddress( test_organizer)) if test_organizer else None test_uid = test_principal.principalUID( ) if test_principal else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_uid == "") and same_calendar_user: continue # Apply a timezone to any floating times fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) # Add extended details if do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) # The calendar may come back as None if the resource is being changed, or was deleted # between our initial index query and getting here. For now we will ignore this error, but in # the longer term we need to implement some form of locking, perhaps. if calendar is None: log.error( "Calendar %s is missing from calendar collection %r" % (name, calresource)) continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_organizer = calendar.getOrganizer() test_principal = ( yield calresource.principalForCalendarUserAddress( test_organizer)) if test_organizer else None test_uid = test_principal.principalUID( ) if test_principal else "" # Check that ORGANIZER's match (security requirement) if (organizer is None) or (organizer_uid == test_uid): continue # Check for no ORGANIZER and check by same calendar user elif (test_organizer is None) and same_calendar_user: continue if filter.match(calendar, None): # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) if calendar.mainType() == "VEVENT": processEventFreeBusy(calendar, fbinfo, timerange, tzinfo) elif calendar.mainType() == "VFREEBUSY": processFreeBusyFreeBusy(calendar, fbinfo, timerange) elif calendar.mainType() == "VAVAILABILITY": processAvailabilityFreeBusy(calendar, fbinfo, timerange) else: assert "Free-busy query returned unwanted component: %s in %r", ( name, calresource, ) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)
class VAlarm(Component): sActionMap = { definitions.cICalProperty_ACTION_AUDIO: definitions.eAction_VAlarm_Audio, definitions.cICalProperty_ACTION_DISPLAY: definitions.eAction_VAlarm_Display, definitions.cICalProperty_ACTION_EMAIL: definitions.eAction_VAlarm_Email, definitions.cICalProperty_ACTION_PROCEDURE: definitions.eAction_VAlarm_Procedure, definitions.cICalProperty_ACTION_URI: definitions.eAction_VAlarm_URI, definitions.cICalProperty_ACTION_NONE: definitions.eAction_VAlarm_None, } sActionValueMap = { definitions.eAction_VAlarm_Audio: definitions.cICalProperty_ACTION_AUDIO, definitions.eAction_VAlarm_Display: definitions.cICalProperty_ACTION_DISPLAY, definitions.eAction_VAlarm_Email: definitions.cICalProperty_ACTION_EMAIL, definitions.eAction_VAlarm_Procedure: definitions.cICalProperty_ACTION_PROCEDURE, definitions.eAction_VAlarm_URI: definitions.cICalProperty_ACTION_URI, definitions.eAction_VAlarm_None: definitions.cICalProperty_ACTION_NONE, } # Classes for each action encapsulating action-specific data class VAlarmAction(object): propertyCardinality_1 = () propertyCardinality_1_Fix_Empty = () propertyCardinality_0_1 = () propertyCardinality_1_More = () def __init__(self, type): self.mType = type def duplicate(self): return VAlarm.VAlarmAction(self.mType) def load(self, valarm): pass def add(self, valarm): pass def remove(self, valarm): pass def getType(self): return self.mType class VAlarmAudio(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ATTACH, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self, speak=None): super(VAlarm.VAlarmAudio, self).__init__(type=definitions.eAction_VAlarm_Audio) self.mSpeakText = speak def duplicate(self): return VAlarm.VAlarmAudio(self.mSpeakText) def load(self, valarm): # Get properties self.mSpeakText = valarm.loadValueString( definitions.cICalProperty_ACTION_X_SPEAKTEXT) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_ACTION_X_SPEAKTEXT, self.mSpeakText) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties( definitions.cICalProperty_ACTION_X_SPEAKTEXT) def isSpeakText(self): return len(self.mSpeakText) != 0 def getSpeakText(self): return self.mSpeakText class VAlarmDisplay(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_1_Fix_Empty = ( definitions.cICalProperty_DESCRIPTION, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self, description=None): super(VAlarm.VAlarmDisplay, self).__init__(type=definitions.eAction_VAlarm_Display) self.mDescription = description def duplicate(self): return VAlarm.VAlarmDisplay(self.mDescription) def load(self, valarm): # Get properties self.mDescription = valarm.loadValueString( definitions.cICalProperty_DESCRIPTION) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_DESCRIPTION, self.mDescription) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties(definitions.cICalProperty_DESCRIPTION) def getDescription(self): return self.mDescription class VAlarmEmail(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_1_Fix_Empty = ( definitions.cICalProperty_DESCRIPTION, definitions.cICalProperty_SUMMARY, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) propertyCardinality_1_More = (definitions.cICalProperty_ATTENDEE, ) def __init__(self, description=None, summary=None, attendees=None): super(VAlarm.VAlarmEmail, self).__init__(type=definitions.eAction_VAlarm_Email) self.mDescription = description self.mSummary = summary self.mAttendees = attendees def duplicate(self): return VAlarm.VAlarmEmail(self.mDescription, self.mSummary, self.mAttendees) def load(self, valarm): # Get properties self.mDescription = valarm.loadValueString( definitions.cICalProperty_DESCRIPTION) self.mSummary = valarm.loadValueString( definitions.cICalProperty_SUMMARY) self.mAttendees = [] if valarm.hasProperty(definitions.cICalProperty_ATTENDEE): # Get each attendee range = valarm.getProperties().get( definitions.cICalProperty_ATTENDEE, ()) for iter in range: # Get the attendee value attendee = iter.getCalAddressValue() if attendee is not None: self.mAttendees.append(attendee.getValue()) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_DESCRIPTION, self.mDescription) valarm.addProperty(prop) prop = Property(definitions.cICalProperty_SUMMARY, self.mSummary) valarm.addProperty(prop) for iter in self.mAttendees: prop = Property(definitions.cICalProperty_ATTENDEE, iter, Value.VALUETYPE_CALADDRESS) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties(definitions.cICalProperty_DESCRIPTION) valarm.removeProperties(definitions.cICalProperty_SUMMARY) valarm.removeProperties(definitions.cICalProperty_ATTENDEE) def getDescription(self): return self.mDescription def getSummary(self): return self.mSummary def getAttendees(self): return self.mAttendees class VAlarmUnknown(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self): super(VAlarm.VAlarmUnknown, self).__init__(type=definitions.eAction_VAlarm_Unknown) def duplicate(self): return VAlarm.VAlarmUnknown() class VAlarmURI(VAlarmAction): propertyCardinality_1 = ( definitions.cICalProperty_ACTION, definitions.cICalProperty_TRIGGER, definitions.cICalProperty_URL, ) propertyCardinality_0_1 = ( definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, definitions.cICalProperty_ACKNOWLEDGED, ) def __init__(self, uri=None): super(VAlarm.VAlarmURI, self).__init__(type=definitions.eAction_VAlarm_URI) self.mURI = uri def duplicate(self): return VAlarm.VAlarmURI(self.mURI) def load(self, valarm): # Get properties self.mURI = valarm.loadValueString(definitions.cICalProperty_URL) def add(self, valarm): # Delete existing then add self.remove(valarm) prop = Property(definitions.cICalProperty_URL, self.mURI) valarm.addProperty(prop) def remove(self, valarm): valarm.removeProperties(definitions.cICalProperty_URL) def getURI(self): return self.mURI class VAlarmNone(VAlarmAction): propertyCardinality_1 = (definitions.cICalProperty_ACTION, ) def __init__(self): super(VAlarm.VAlarmNone, self).__init__(type=definitions.eAction_VAlarm_None) def duplicate(self): return VAlarm.VAlarmNone() def getMimeComponentName(self): # Cannot be sent as a separate MIME object return None sActionToAlarmMap = { definitions.eAction_VAlarm_Audio: VAlarmAudio, definitions.eAction_VAlarm_Display: VAlarmDisplay, definitions.eAction_VAlarm_Email: VAlarmEmail, definitions.eAction_VAlarm_URI: VAlarmURI, definitions.eAction_VAlarm_None: VAlarmNone, } propertyValueChecks = ICALENDAR_VALUE_CHECKS def __init__(self, parent=None): super(VAlarm, self).__init__(parent=parent) self.mAction = definitions.eAction_VAlarm_Display self.mTriggerAbsolute = False self.mTriggerOnStart = True self.mTriggerOn = DateTime() # Set duration default to 1 hour self.mTriggerBy = Duration() self.mTriggerBy.setDuration(60 * 60) # Does not repeat by default self.mRepeats = 0 self.mRepeatInterval = Duration() self.mRepeatInterval.setDuration(5 * 60) # Five minutes # Status self.mStatusInit = False self.mAlarmStatus = definitions.eAlarm_Status_Pending self.mLastTrigger = DateTime() self.mNextTrigger = DateTime() self.mDoneCount = 0 # Create action data self.mActionData = VAlarm.VAlarmDisplay("") def duplicate(self, parent=None): other = super(VAlarm, self).duplicate(parent=parent) other.mAction = self.mAction other.mTriggerAbsolute = self.mTriggerAbsolute other.mTriggerOn = self.mTriggerOn.duplicate() other.mTriggerBy = self.mTriggerBy.duplicate() other.mTriggerOnStart = self.mTriggerOnStart other.mRepeats = self.mRepeats other.mRepeatInterval = self.mRepeatInterval.duplicate() other.mAlarmStatus = self.mAlarmStatus if self.mLastTrigger is not None: other.mLastTrigger = self.mLastTrigger.duplicate() if self.mNextTrigger is not None: other.mNextTrigger = self.mNextTrigger.duplicate() other.mDoneCount = self.mDoneCount other.mActionData = self.mActionData.duplicate() return other def getType(self): return definitions.cICalComponent_VALARM def getAction(self): return self.mAction def getActionData(self): return self.mActionData def isTriggerAbsolute(self): return self.mTriggerAbsolute def getTriggerOn(self): return self.mTriggerOn def getTriggerDuration(self): return self.mTriggerBy def isTriggerOnStart(self): return self.mTriggerOnStart def getRepeats(self): return self.mRepeats def getInterval(self): return self.mRepeatInterval def added(self): # Added to calendar so add to calendar notifier # calstore::CCalendarNotifier::sCalendarNotifier.AddAlarm(this) # Do inherited super(VAlarm, self).added() def removed(self): # Removed from calendar so add to calendar notifier # calstore::CCalendarNotifier::sCalendarNotifier.RemoveAlarm(this) # Do inherited super(VAlarm, self).removed() def changed(self): # Always force recalc of trigger status self.mStatusInit = False # Changed in calendar so change in calendar notifier # calstore::CCalendarNotifier::sCalendarNotifier.ChangedAlarm(this) # Do not do inherited as this is always a sub-component and we do not # do top-level component changes # super.changed() def finalise(self): # Do inherited super(VAlarm, self).finalise() # Get the ACTION temp = self.loadValueString(definitions.cICalProperty_ACTION) if temp is not None: self.mAction = VAlarm.sActionMap.get( temp, definitions.eAction_VAlarm_Unknown) self.loadAction() # Get the trigger if self.hasProperty(definitions.cICalProperty_TRIGGER): # Determine the type of the value temp1 = self.loadValueDateTime(definitions.cICalProperty_TRIGGER) temp2 = self.loadValueDuration(definitions.cICalProperty_TRIGGER) if temp1 is not None: self.mTriggerAbsolute = True self.mTriggerOn = temp1 elif temp2 is not None: self.mTriggerAbsolute = False self.mTriggerBy = temp2 # Get the property prop = self.findFirstProperty( definitions.cICalProperty_TRIGGER) # Look for RELATED parameter if prop.hasParameter(definitions.cICalParameter_RELATED): temp = prop.getParameterValue( definitions.cICalParameter_RELATED) if temp == definitions.cICalParameter_RELATED_START: self.mTriggerOnStart = True elif temp == definitions.cICalParameter_RELATED_END: self.mTriggerOnStart = False else: self.mTriggerOnStart = True # Get repeat & interval temp = self.loadValueInteger(definitions.cICalProperty_REPEAT) if temp is not None: self.mRepeats = temp temp = self.loadValueDuration(definitions.cICalProperty_DURATION) if temp is not None: self.mRepeatInterval = temp # Set a map key for sorting self.mMapKey = "%s:%s" % ( self.mAction, self.mTriggerOn if self.mTriggerAbsolute else self.mTriggerBy, ) # Alarm status - private to Mulberry status = self.loadValueString( definitions.cICalProperty_ALARM_X_ALARMSTATUS) if status is not None: if status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING: self.mAlarmStatus = definitions.eAlarm_Status_Pending elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED: self.mAlarmStatus = definitions.eAlarm_Status_Completed elif status == definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED: self.mAlarmStatus = definitions.eAlarm_Status_Disabled else: self.mAlarmStatus = definitions.eAlarm_Status_Pending # Last trigger time - private to Mulberry temp = self.loadValueDateTime( definitions.cICalProperty_ALARM_X_LASTTRIGGER) if temp is not None: self.mLastTrigger = temp def validate(self, doFix=False): """ Validate the data in this component and optionally fix any problems, else raise. If loggedProblems is not None it must be a C{list} and problem descriptions are appended to that. """ # Validate using action specific constraints self.propertyCardinality_1 = self.mActionData.propertyCardinality_1 self.propertyCardinality_1_Fix_Empty = self.mActionData.propertyCardinality_1_Fix_Empty self.propertyCardinality_0_1 = self.mActionData.propertyCardinality_0_1 self.propertyCardinality_1_More = self.mActionData.propertyCardinality_1_More fixed, unfixed = super(VAlarm, self).validate(doFix) # Extra constraint: both DURATION and REPEAT must be present togethe if self.hasProperty( definitions.cICalProperty_DURATION) ^ self.hasProperty( definitions.cICalProperty_REPEAT): # Cannot fix this logProblem = "[%s] Properties must be present together: %s, %s" % ( self.getType(), definitions.cICalProperty_DURATION, definitions.cICalProperty_REPEAT, ) unfixed.append(logProblem) return fixed, unfixed def editStatus(self, status): # Remove existing self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS) # Updated cached values self.mAlarmStatus = status # Add new status_txt = "" if self.mAlarmStatus == definitions.eAlarm_Status_Pending: status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING elif self.mAlarmStatus == definitions.eAlarm_Status_Completed: status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled: status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED self.addProperty( Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status_txt)) def editAction(self, action, data): # Remove existing self.removeProperties(definitions.cICalProperty_ACTION) self.mActionData.remove(self) self.mActionData = None # Updated cached values self.mAction = action self.mActionData = data # Add new properties to alarm action_txt = VAlarm.sActionValueMap.get( self.mAction, definitions.cICalProperty_ACTION_PROCEDURE) prop = Property(definitions.cICalProperty_ACTION, action_txt) self.addProperty(prop) self.mActionData.add(self) def editTriggerOn(self, dt): # Remove existing self.removeProperties(definitions.cICalProperty_TRIGGER) # Updated cached values self.mTriggerAbsolute = True self.mTriggerOn = dt # Add new prop = Property(definitions.cICalProperty_TRIGGER, dt) self.addProperty(prop) def editTriggerBy(self, duration, trigger_start): # Remove existing self.removeProperties(definitions.cICalProperty_TRIGGER) # Updated cached values self.mTriggerAbsolute = False self.mTriggerBy = duration self.mTriggerOnStart = trigger_start # Add new (with parameter) prop = Property(definitions.cICalProperty_TRIGGER, duration) attr = Parameter( definitions.cICalParameter_RELATED, (definitions.cICalParameter_RELATED_START, definitions.cICalParameter_RELATED_END)[not trigger_start]) prop.addParameter(attr) self.addProperty(prop) def editRepeats(self, repeat, interval): # Remove existing self.removeProperties(definitions.cICalProperty_REPEAT) self.removeProperties(definitions.cICalProperty_DURATION) # Updated cached values self.mRepeats = repeat self.mRepeatInterval = interval # Add new if self.mRepeats > 0: self.addProperty(Property(definitions.cICalProperty_REPEAT, repeat)) self.addProperty( Property(definitions.cICalProperty_DURATION, interval)) def getAlarmStatus(self): return self.mAlarmStatus def getNextTrigger(self, dt): if not self.mStatusInit: self.initNextTrigger() dt.copy(self.mNextTrigger) def alarmTriggered(self, dt): # Remove existing self.removeProperties(definitions.cICalProperty_ALARM_X_LASTTRIGGER) self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS) # Updated cached values self.mLastTrigger.copy(dt) if self.mDoneCount < self.mRepeats: self.mNextTrigger = self.mLastTrigger + self.mRepeatInterval dt.copy(self.mNextTrigger) self.mDoneCount += 1 self.mAlarmStatus = definitions.eAlarm_Status_Pending else: self.mAlarmStatus = definitions.eAlarm_Status_Completed # Add new self.addProperty( Property(definitions.cICalProperty_ALARM_X_LASTTRIGGER, dt)) status = "" if self.mAlarmStatus == definitions.eAlarm_Status_Pending: status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_PENDING elif self.mAlarmStatus == definitions.eAlarm_Status_Completed: status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_COMPLETED elif self.mAlarmStatus == definitions.eAlarm_Status_Disabled: status = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED self.addProperty( Property(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status)) # Now update dt to the next alarm time return self.mAlarmStatus == definitions.eAlarm_Status_Pending def loadAction(self): # Delete current one self.mActionData = None self.mActionData = VAlarm.sActionToAlarmMap.get( self.mAction, VAlarm.VAlarmUnknown)() self.mActionData.load(self) def initNextTrigger(self): # Do not bother if its completed if self.mAlarmStatus == definitions.eAlarm_Status_Completed: return self.mStatusInit = True # Look for trigger immediately preceeding or equal to utc now nowutc = DateTime.getNowUTC() # Init done counter self.mDoneCount = 0 # Determine the first trigger trigger = DateTime() self.getFirstTrigger(trigger) while self.mDoneCount < self.mRepeats: # See if next trigger is later than now next_trigger = trigger + self.mRepeatInterval if next_trigger > nowutc: break self.mDoneCount += 1 trigger = next_trigger # Check for completion if trigger == self.mLastTrigger or ( nowutc - trigger).getTotalSeconds() > 24 * 60 * 60: if self.mDoneCount == self.mRepeats: self.mAlarmStatus = definitions.eAlarm_Status_Completed return else: trigger = trigger + self.mRepeatInterval self.mDoneCount += 1 self.mNextTrigger = trigger def getFirstTrigger(self, dt): # If absolute trigger, use that if self.isTriggerAbsolute(): # Get the trigger on dt.copy(self.getTriggerOn()) else: # Get the parent embedder class (must be CICalendarComponentRecur type) owner = self.getEmbedder() if owner is not None: # Determine time at which alarm will trigger trigger = (owner.getStart(), owner.getEnd())[not self.isTriggerOnStart()] # Offset by duration dt.copy(trigger + self.getTriggerDuration())