Esempio n. 1
0
    def _getNormalizedDateTimeProperties(self, component):

        # Basic time properties
        if component.name() in ("VEVENT", "VJOURNAL", "VPOLL"):
            dtstart = component.getProperty("DTSTART")
            dtend = component.getProperty("DTEND")
            duration = component.getProperty("DURATION")

            timeRange = Period(
                start=dtstart.value() if dtstart is not None else None,
                end=dtend.value() if dtend is not None else None,
                duration=duration.value() if duration is not None else None,
            )
            newdue = None

        elif component.name() == "VTODO":
            dtstart = component.getProperty("DTSTART")
            duration = component.getProperty("DURATION")

            if dtstart or duration:
                timeRange = Period(
                    start=dtstart.value() if dtstart is not None else None,
                    duration=duration.value() if duration is not None else None,
                )
            else:
                timeRange = Period()

            newdue = component.getProperty("DUE")
            if newdue is not None:
                newdue = newdue.value().duplicate().adjustToUTC()
        else:
            timeRange = Period()
            newdue = None

        # Recurrence rules - we need to normalize the order of the value parts
        newrrules = set()
        rrules = component.properties("RRULE")
        for rrule in rrules:
            indexedTokens = {}
            indexedTokens.update([valuePart.split("=") for valuePart in rrule.value().getText().split(";")])
            sortedValue = ";".join(["%s=%s" % (key, value,) for key, value in sorted(indexedTokens.iteritems(), key=lambda x:x[0])])
            newrrules.add(sortedValue)

        # RDATEs
        newrdates = set()
        rdates = component.properties("RDATE")
        for rdate in rdates:
            for value in rdate.value():
                if isinstance(value, DateTime):
                    value = value.duplicate().adjustToUTC()
                newrdates.add(value)

        # EXDATEs
        newexdates = set()
        exdates = component.properties("EXDATE")
        for exdate in exdates:
            newexdates.update([exvalue.getValue().duplicate().adjustToUTC() for exvalue in exdate.value()])

        return timeRange.getStart(), timeRange.getEnd(), newdue, newrrules, newrdates, newexdates
Esempio n. 2
0
    def getVFreeBusyFB(self, period, fb):
        # First create expanded set
        # TODO: fix this
        # list = ExpandedComponents()
        self.getVEvents(period, list)
        if len(list) == 0:
            return

        # Get start/end list for each non-all-day expanded components
        dtstart = []
        dtend = []
        for dt in list:

            # Ignore if all-day
            if dt.getInstanceStart().isDateOnly():
                continue

            # Ignore if transparent to free-busy
            transp = ""
            if dt.getOwner().getProperty(
                    definitions.cICalProperty_TRANSP,
                    transp) and (transp
                                 == definitions.cICalProperty_TRANSPARENT):
                continue

            # Add start/end to list
            dtstart.append(dt.getInstanceStart())
            dtend.append(dt.getInstanceEnd())

        # No longer need the expanded items
        list.clear()

        # Create non-overlapping periods as properties in the freebusy component
        temp = Period(dtstart.front(), dtend.front())
        dtstart_iter = dtstart.iter()
        dtstart_iter.next()
        dtend_iter = dtend.iter()
        dtend_iter.next()
        for i in i:

            # Check for non-overlap
            if dtstart_iter > temp.getEnd():

                # Current period is complete
                fb.addProperty(
                    Property(definitions.cICalProperty_FREEBUSY, temp))

                # Reset period to new range
                temp = Period(dtstart_iter, dtend_iter)

            # They overlap - check for extended end
            if dtend_iter > temp.getEnd():

                # Extend the end
                temp = Period(temp.getStart(), dtend_iter)

        # Add remaining period as property
        fb.addProperty(Property(definitions.cICalProperty_FREEBUSY, temp))
Esempio n. 3
0
    def getFreeBusy(self, period, fb):
        # First create expanded set

        list = []
        self.getVEvents(period, list)

        # Get start/end list for each non-all-day expanded components
        for comp in list:

            # Ignore if all-day
            if comp.getInstanceStart().isDateOnly():
                continue

            # Ignore if transparent to free-busy
            transp = ""
            if comp.getOwner().getProperty(
                    definitions.cICalProperty_TRANSP,
                    transp) and (transp
                                 == definitions.cICalProperty_TRANSPARENT):
                continue

            # Add free busy item to list
            status = comp.getMaster().getStatus()
            if status in (definitions.eStatus_VEvent_None,
                          definitions.eStatus_VEvent_Confirmed):
                fb.append(
                    FreeBusy(
                        FreeBusy.BUSY,
                        Period(comp.getInstanceStart(),
                               comp.getInstanceEnd())))
            elif status == definitions.eStatus_VEvent_Tentative:
                fb.append(
                    FreeBusy(
                        FreeBusy.BUSYTENTATIVE,
                        Period(comp.getInstanceStart(),
                               comp.getInstanceEnd())))
                break
            elif status == definitions.eStatus_VEvent_Cancelled:
                # Cancelled => does not contribute to busy time
                pass

        # Now get the VFREEBUSY info
        list2 = []
        self.getVFreeBusy(period, list2)

        # Get start/end list for each free-busy
        for comp in list2:

            # Expand component and add free busy info to list
            comp.expandPeriod(period, fb)

        # Add remaining period as property
        FreeBusy.resolveOverlaps(fb)
Esempio n. 4
0
    def processAvailabilityFreeBusy(self, calendar, fbinfo):
        """
        Extract free-busy data from a VAVAILABILITY component.

        @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
        @param fbinfo: the tuple used to store the three types of fb data.
        """

        for vav in [
                x for x in calendar.subcomponents()
                if x.name() == "VAVAILABILITY"
        ]:

            # Get overall start/end
            start = vav.getStartDateUTC()
            if start is None:
                start = DateTime(1900,
                                 1,
                                 1,
                                 0,
                                 0,
                                 0,
                                 tzid=Timezone.UTCTimezone)
            end = vav.getEndDateUTC()
            if end is None:
                end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone.UTCTimezone)
            period = Period(start, end)
            overall = clipPeriod(period, self.timerange)
            if overall is None:
                continue

            # Now get periods for each instance of AVAILABLE sub-components
            periods = self.processAvailablePeriods(vav)

            # Now invert the periods and store in accumulator
            busyperiods = []
            last_end = self.timerange.getStart()
            for period in periods:
                if last_end < period.getStart():
                    busyperiods.append(Period(last_end, period.getStart()))
                last_end = period.getEnd()
            if last_end < self.timerange.getEnd():
                busyperiods.append(Period(last_end, self.timerange.getEnd()))

            # Add to actual results mapped by busy type
            fbtype = vav.propertyValue("BUSYTYPE")
            if fbtype is None:
                fbtype = "BUSY-UNAVAILABLE"

            getattr(fbinfo,
                    self.FBInfo_mapper.get(fbtype,
                                           "unavailable")).extend(busyperiods)
Esempio n. 5
0
    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")
Esempio n. 6
0
    def test_freebusy(self):
        """
        Test that action=component works.
        """

        yield self.createShare("user01", "puser01")

        calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar")
        yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
        yield self.commitTransaction(0)

        fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
        fbend = "{now:04d}0103T000000Z".format(**self.nowYear)

        shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")

        fbinfo = FreebusyQuery.FBInfo([], [], [])
        timerange = Period(DateTime.parseText(fbstart), DateTime.parseText(fbend))
        organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:[email protected]", self.theTransactionUnderTest(1)))

        freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange)
        matchtotal = (yield freebusy.generateFreeBusyInfo([shared, ], fbinfo))

        self.assertEqual(matchtotal, 1)
        self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
        yield self.commitTransaction(1)
    def testMonthlyInUTC(self):

        recur = Recurrence()
        recur.parse("FREQ=MONTHLY")
        start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
        end = DateTime(2015, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
        items = []
        range = Period(start, end)
        recur.expand(DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items)
        self.assertEqual(
            items,
            [
                DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 2, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 3, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 4, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 5, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 6, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 7, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 8, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 9, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 10, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 11, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 12, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
            ],
        )
Esempio n. 8
0
    def expandAll(self, start, end, with_name):

        if start is None:
            start = self.mStart

        # Ignore if there is no change in offset
        offsetto = self.loadValueInteger(definitions.cICalProperty_TZOFFSETTO, Value.VALUETYPE_UTC_OFFSET)
        offsetfrom = self.loadValueInteger(definitions.cICalProperty_TZOFFSETFROM, Value.VALUETYPE_UTC_OFFSET)
#        if offsetto == offsetfrom:
#            return ()

        # Look for recurrences
        if self.mStart > end:
            # Return nothing
            return ()
        elif not self.mRecurrences.hasRecurrence():
            # Return DTSTART even if it is newer
            if self.mStart >= start:
                result = (self.mStart, offsetfrom, offsetto,)
                if with_name:
                    result += (self.getTZName(),)
                return (result,)
            else:
                return ()
        else:
            # We want to allow recurrence calculation caching to help us here
            # as this method
            # gets called a lot - most likely for ever increasing dt values
            # (which will therefore
            # invalidate the recurrence cache).
            #
            # What we will do is round up the date-time to the next year so
            # that the recurrence
            # cache is invalidated less frequently

            temp = DateTime(end.getYear(), 1, 1, 0, 0, 0)

            # Use cache of expansion
            if self.mCachedExpandBelowItems is None:
                self.mCachedExpandBelowItems = []
            if self.mCachedExpandBelow is None:
                self.mCachedExpandBelow = self.mStart.duplicate()
            if temp > self.mCachedExpandBelow:
                self.mCachedExpandBelowItems = []
                period = Period(self.mStart, end)
                self.mRecurrences.expand(self.mStart, period, self.mCachedExpandBelowItems, float_offset=self.mUTCOffsetFrom)
                self.mCachedExpandBelow = temp

            if len(self.mCachedExpandBelowItems) != 0:
                # Return them all within the range
                results = []
                for dt in self.mCachedExpandBelowItems:
                    if dt >= start and dt < end:
                        result = (dt, offsetfrom, offsetto,)
                        if with_name:
                            result += (self.getTZName(),)
                        results.append(result)
                return results

            return ()
Esempio n. 9
0
    def limitFreeBusy(self, calendar):
        """
        Limit the range of any FREEBUSY properties in the calendar, returning
        a new calendar if limits were applied, or the same one if no limits were applied.
        @param calendar: the L{Component} for the calendar to operate on.
        @return: the L{Component} for the result.
        """

        # First check for any VFREEBUSYs - can ignore limit if there are none
        if calendar.mainType() != "VFREEBUSY":
            return calendar

        # Create duplicate calendar and filter FREEBUSY properties
        calendar = calendar.duplicate()
        for component in calendar.subcomponents():
            if component.name() != "VFREEBUSY":
                continue
            for property in component.properties("FREEBUSY"):
                newvalue = []
                for period in property.value():
                    clipped = clipPeriod(period.getValue(), Period(self.calendardata.freebusy_set.start, self.calendardata.freebusy_set.end))
                    if clipped:
                        newvalue.append(clipped)
                if len(newvalue):
                    property.setValue(newvalue)
                else:
                    component.removeProperty(property)
        return calendar
Esempio n. 10
0
def clipPeriod(period, clipPeriod):
    """
    Clip the start/end period so that it lies entirely within the clip period.
    @param period: the (start, end) tuple for the period to be clipped.
    @param clipPeriod: the (start, end) tuple for the period to clip to.
    @return: the (start, end) tuple for the clipped period, or
             None if the period is outside the clip period
    """
    start = period.getStart()
    end = period.getEnd()
    clipStart = clipPeriod.getStart()
    clipEnd = clipPeriod.getEnd()

    if start < clipStart:
        start = clipStart

    if end > clipEnd:
        end = clipEnd

    if start >= end:
        return None
    else:
        # Try to preserve use of duration in period
        result = Period(start, end)
        result.setUseDuration(period.getUseDuration())
        return result
Esempio n. 11
0
    def test_one_event_event_details(self):
        """
        Test when the calendar is empty.
        """

        data = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:1234-5678
DTSTAMP:20080601T000000Z
DTSTART:%s
DTEND:%s
END:VEVENT
END:VCALENDAR
""" % (
            self.now_12H.getText(),
            self.now_13H.getText(),
        )

        yield self._createCalendarObject(data, "user01", "test.ics")
        calendar = (yield self.calendarUnderTest(home="user01",
                                                 name="calendar_1"))
        fbinfo = FreebusyQuery.FBInfo([], [], [])
        timerange = Period(self.now, self.now_1D)
        event_details = []

        organizer = recipient = yield calendarUserFromCalendarUserAddress(
            "mailto:[email protected]", self.transactionUnderTest())
        freebusy = FreebusyQuery(organizer=organizer,
                                 recipient=recipient,
                                 timerange=timerange,
                                 event_details=event_details)
        freebusy.same_calendar_user = True
        result = yield freebusy.generateFreeBusyInfo([
            calendar,
        ], fbinfo)
        self.assertEqual(result, 1)
        self.assertEqual(fbinfo.busy, [
            Period(self.now_12H, self.now_13H),
        ])
        self.assertEqual(len(fbinfo.tentative), 0)
        self.assertEqual(len(fbinfo.unavailable), 0)
        self.assertEqual(len(event_details), 1)
        self.assertEqual(
            str(event_details[0]),
            str(tuple(Component.fromString(data).subcomponents())[0]))
Esempio n. 12
0
def processAvailablePeriods(calendar, timerange):
    """
    Extract instance period data from an AVAILABLE component.
    @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
    @param timerange: the time range to restrict free busy data to.
    """

    periods = []

    # First we need to group all AVAILABLE sub-components by UID
    uidmap = {}
    for component in calendar.subcomponents():
        if component.name() == "AVAILABLE":
            uid = component.propertyValue("UID")
            uidmap.setdefault(uid, []).append(component)

    # Then we expand each uid set separately
    for componentSet in uidmap.itervalues():
        instances = InstanceList(ignoreInvalidInstances=True)
        instances.expandTimeRanges(componentSet, timerange.end)

        # Now convert instances into period list
        for key in instances:
            instance = instances[key]
            # Ignore any with floating times (which should not happen as the spec requires UTC or local
            # but we will try and be safe here).
            start = instance.start
            if start.floating():
                continue
            end = instance.end
            if end.floating():
                continue

            # Clip period for this instance - use duration for period end if that
            # is what original component used
            if instance.component.hasProperty("DURATION"):
                period = Period(start, duration=end - start)
            else:
                period = Period(start, end)
            clipped = clipPeriod(period, Period(timerange.start,
                                                timerange.end))
            if clipped:
                periods.append(clipped)

    normalizePeriodList(periods)
    return periods
Esempio n. 13
0
def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy):
    """
    Generate a free-busy REPORT.
    (CalDAV-access-09, section 7.8)
    """
    if not self.isCollection():
        log.error("freebusy report is only allowed on collection resources {s!r}", s=self)
        raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Not a calendar collection"))

    if freebusy.qname() != (caldavxml.caldav_namespace, "free-busy-query"):
        raise ValueError("{CalDAV:}free-busy-query expected as root element, not %s." % (freebusy.sname(),))

    timerange = freebusy.timerange
    if not timerange.valid():
        raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified"))

    fbset = []

    accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
    if accepted_type is None:
        raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))

    def getCalendarList(calresource, uri):  # @UnusedVariable
        """
        Store the calendars that match the query in L{fbset} which will then be used with the
        freebusy query.

        @param calresource: the L{CalDAVResource} for a calendar collection.
        @param uri: the uri for the calendar collection resource.
        """

        fbset.append(calresource._newStoreObject)
        return succeed(True)

    # Run report taking depth into account
    depth = request.headers.getHeader("depth", "0")
    yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),))

    # Do the actual freebusy query against the set of matched calendars
    principal = yield self.resourceOwnerPrincipal(request)
    organizer = recipient = LocalCalendarUser(principal.canonicalCalendarUserAddress(), principal.record)
    timerange = Period(timerange.start, timerange.end)
    try:
        fbresult = yield FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None)
    except NumberOfMatchesWithinLimits:
        log.error("Too many matching components in free-busy report")
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            davxml.NumberOfMatchesWithinLimits(),
            "Too many components"
        ))
    except TimeRangeLowerLimit, e:
        raise HTTPError(ErrorResponse(
            responsecode.FORBIDDEN,
            caldavxml.MinDateTime(),
            "Time-range value too far in the past. Must be on or after %s." % (str(e.limit),)
        ))
Esempio n. 14
0
def normalizePeriodList(periods):
    """
    Normalize the list of periods by merging overlapping or consecutive ranges
    and sorting the list by each periods start.
    @param list: a list of tuples of L{Period}. The list is changed in place.
    """

    # First sort the list
    def sortPeriods(p1, p2):
        """
        Compare two periods. Sort by their start and then end times.
        A period is a L{Period}.
        @param p1: first period
        @param p2: second period
        @return: 1 if p1>p2, 0 if p1==p2, -1 if p1<p2
        """

        assert isinstance(p1, Period), "Period is not a Period: %r" % (p1,)
        assert isinstance(p2, Period), "Period is not a Period: %r" % (p2,)

        if p1.getStart() == p2.getStart():
            cmp1 = p1.getEnd()
            cmp2 = p2.getEnd()
        else:
            cmp1 = p1.getStart()
            cmp2 = p2.getStart()

        return compareDateTime(cmp1, cmp2)

    for period in periods:
        period.adjustToUTC()
    periods.sort(cmp=sortPeriods)

    # Now merge overlaps and consecutive periods
    index = None
    p = None
    pe = None
    for i in xrange(len(periods)):
        if p is None:
            index = i
            p = periods[i]
            pe = p.getEnd()
            continue
        ie = periods[i].getEnd()
        if (pe >= periods[i].getStart()):
            if ie > pe:
                periods[index] = Period(periods[index].getStart(), ie)
                pe = ie
            periods[i] = None
        else:
            index = i
            p = periods[i]
            pe = p.getEnd()
    periods[:] = [x for x in periods if x]
Esempio n. 15
0
    def testWeeklyTwice(self):

        recur = Recurrence()
        recur.parse("FREQ=WEEKLY")
        start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
        end = DateTime(2014, 2, 1, 0, 0, 0, tzid=Timezone(utc=True))
        items = []
        range = Period(start, end)
        recur.expand(DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items)
        self.assertEqual(
            items,
            [
                DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),

            ],
        )

        start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
        end = DateTime(2014, 3, 1, 0, 0, 0, tzid=Timezone(utc=True))
        items = []
        range = Period(start, end)
        recur.expand(DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items)
        self.assertEqual(
            items,
            [
                DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 2, 5, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 2, 12, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 2, 19, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
                DateTime(2014, 2, 26, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
            ],
        )
Esempio n. 16
0
def instances(start, rrule):
    """
    Expand an RRULE.
    """

    recur = Recurrence()
    recur.parse(rrule)
    start = DateTime.parseText(start)
    end = start.duplicate()
    end.offsetYear(100)
    items = []
    range = Period(start, end)
    recur.expand(start, range, items)
    print("DTSTART:{}".format(start))
    print("RRULE:{}".format(rrule))
    print("Instances: {}".format(", ".join(map(str, items))))
Esempio n. 17
0
    def expandBelow(self, below):

        # Look for recurrences
        if not self.mRecurrences.hasRecurrence() or self.mStart > below:
            # Return DTSTART even if it is newer
            return self.mStart
        else:
            # We want to allow recurrence calculation caching to help us here
            # as this method
            # gets called a lot - most likely for ever increasing dt values
            # (which will therefore
            # invalidate the recurrence cache).
            #
            # What we will do is round up the date-time to the next year so
            # that the recurrence
            # cache is invalidated less frequently

            temp = DateTime(below.getYear(), 1, 1, 0, 0, 0)

            # Use cache of expansion
            if self.mCachedExpandBelowItems is None:
                self.mCachedExpandBelowItems = []
            if self.mCachedExpandBelow is None:
                self.mCachedExpandBelow = self.mStart.duplicate()
            if temp > self.mCachedExpandBelow:
                self.mCachedExpandBelowItems = []
                period = Period(self.mStart, temp)
                self.mRecurrences.expand(self.mStart,
                                         period,
                                         self.mCachedExpandBelowItems,
                                         float_offset=self.mUTCOffsetFrom)
                self.mCachedExpandBelow = temp

            if len(self.mCachedExpandBelowItems) != 0:
                # List comes back sorted so we pick the element just less than
                # the dt value we want
                i = bisect_right(self.mCachedExpandBelowItems, below)
                if i != 0:
                    return self.mCachedExpandBelowItems[i - 1]

                # The first one in the list is the one we want
                return self.mCachedExpandBelowItems[0]

            return self.mStart
Esempio n. 18
0
    def testClearOnChange(self):

        recur = Recurrence()
        recur.parse("FREQ=DAILY")

        start = DateTime(2013, 1, 1, 0, 0, 0)
        end = DateTime(2017, 1, 1, 0, 0, 0)
        range = Period(start, end)
        items = []
        recur.expand(start, range, items)
        self.assertTrue(recur.mCached)
        self.assertTrue(len(items) > 100)

        recur.setUseCount(True)
        recur.setCount(10)
        self.assertFalse(recur.mCached)
        items = []
        recur.expand(start, range, items)
        self.assertEqual(len(items), 10)
Esempio n. 19
0
    def testExampleRules(self):

        examples = os.path.join(os.path.dirname(__file__),
                                "rrule_examples.json")
        with open(examples) as f:
            examples = json.loads(f.read())

        for ctr, i in enumerate(examples):

            recur = Recurrence()
            recur.parse(i["rule"])
            start = DateTime.parseText(i["start"])
            end = DateTime.parseText(i["end"])
            results = map(DateTime.parseText, i["results"])

            items = []
            range = Period(start, end)
            recur.expand(start, range, items)
            self.assertEqual(items,
                             results,
                             msg="Failed rule: #{} {}".format(
                                 ctr + 1, i["rule"]))
Esempio n. 20
0
    def test_no_events(self):
        """
        Test when the calendar is empty.
        """

        calendar = (yield self.calendarUnderTest(home="user01",
                                                 name="calendar_1"))
        fbinfo = FreebusyQuery.FBInfo([], [], [])
        timerange = Period(self.now, self.now_1D)

        organizer = recipient = yield calendarUserFromCalendarUserAddress(
            "mailto:[email protected]", self.transactionUnderTest())
        freebusy = FreebusyQuery(organizer=organizer,
                                 recipient=recipient,
                                 timerange=timerange)
        result = (yield freebusy.generateFreeBusyInfo([
            calendar,
        ], fbinfo))
        self.assertEqual(result, 0)
        self.assertEqual(len(fbinfo.busy), 0)
        self.assertEqual(len(fbinfo.tentative), 0)
        self.assertEqual(len(fbinfo.unavailable), 0)
Esempio n. 21
0
def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
    """
    Extract FREEBUSY data from a VFREEBUSY component.
    @param calendar: the L{Component} that is the VCALENDAR containing the VFREEBUSY's.
    @param fbinfo: the tuple used to store the three types of fb data.
    @param timerange: the time range to restrict free busy data to.
    """

    for vfb in [
            x for x in calendar.subcomponents() if x.name() == "VFREEBUSY"
    ]:
        # First check any start/end in the actual component
        start = vfb.getStartDateUTC()
        end = vfb.getEndDateUTC()
        if start and end:
            if not timeRangesOverlap(start, end, timerange.start,
                                     timerange.end):
                continue

        # Now look at each FREEBUSY property
        for fb in vfb.properties("FREEBUSY"):
            # Check the type
            fbtype = fb.parameterValue("FBTYPE", default="BUSY")
            if fbtype == "FREE":
                continue

            # Look at each period in the property
            assert isinstance(
                fb.value(), list
            ), "FREEBUSY property does not contain a list of values: %r" % (
                fb, )
            for period in fb.value():
                # Clip period for this instance
                clipped = clipPeriod(period.getValue(),
                                     Period(timerange.start, timerange.end))
                if clipped:
                    fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
Esempio n. 22
0
    def _addMasterComponent(self, component, lowerLimit, upperlimit, rulestart,
                            start, end, duration):

        rrules = component.getRecurrenceSet()
        if rrules is not None and rulestart is not None:
            # Do recurrence set expansion
            expanded = []
            # Begin expansion far in the past because there may be RDATEs earlier
            # than the master DTSTART, and if we exclude those, the associated
            # overridden instances will cause an InvalidOverriddenInstance.
            limited = rrules.expand(rulestart,
                                    Period(DateTime(1900, 1, 1), upperlimit),
                                    expanded)
            for startDate in expanded:
                startDate = self.normalizeFunction(startDate)
                endDate = startDate + duration
                if lowerLimit is None or endDate >= lowerLimit:
                    self.addInstance(Instance(component, startDate, endDate))
                else:
                    self.lowerLimit = lowerLimit
            if limited:
                self.limit = upperlimit
        else:
            # Always add main instance if included in range.
            if start < upperlimit:
                if lowerLimit is None or end >= lowerLimit:
                    start = self.normalizeFunction(start)
                    end = self.normalizeFunction(end)
                    self.addInstance(Instance(component, start, end))
                else:
                    self.lowerLimit = lowerLimit
            else:
                self.limit = upperlimit

        self.master_cancelled = component.propertyValue(
            "STATUS") == "CANCELLED"
Esempio n. 23
0
    def testGetVEvents(self):

        data = (
            (
                "Non-recurring match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART;VALUE=DATE:20110601
DURATION:P1D
DTSTAMP:20020101T000000Z
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (DateTime(2011, 6, 1), ),
            ),
            (
                "Non-recurring no-match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART;VALUE=DATE:20110501
DURATION:P1D
DTSTAMP:20020101T000000Z
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (),
            ),
            (
                "Recurring match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART;VALUE=DATE:20110601
DURATION:P1D
DTSTAMP:20020101T000000Z
RRULE:FREQ=DAILY;COUNT=2
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (
                    DateTime(2011, 6, 1),
                    DateTime(2011, 6, 2),
                ),
            ),
            (
                "Recurring no match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART;VALUE=DATE:20110501
DURATION:P1D
DTSTAMP:20020101T000000Z
RRULE:FREQ=DAILY;COUNT=2
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (),
            ),
            (
                "Recurring with override match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART:20110601T120000
DURATION:P1D
DTSTAMP:20020101T000000Z
RRULE:FREQ=DAILY;COUNT=2
SUMMARY:New Year's Day
END:VEVENT
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
RECURRENCE-ID;VALUE=DATE:20110602T120000
DTSTART;VALUE=DATE:20110602T130000
DURATION:P1D
DTSTAMP:20020101T000000Z
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (
                    DateTime(2011, 6, 1, 12, 0, 0),
                    DateTime(2011, 6, 2, 13, 0, 0),
                ),
            ),
            (
                "Recurring with override no match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART:20110501T120000
DURATION:P1D
DTSTAMP:20020101T000000Z
RRULE:FREQ=DAILY;COUNT=2
SUMMARY:New Year's Day
END:VEVENT
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
RECURRENCE-ID;VALUE=DATE:20110502T120000
DTSTART;VALUE=DATE:20110502T130000
DURATION:P1D
DTSTAMP:20020101T000000Z
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (),
            ),
            (
                "Recurring partial match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART;VALUE=DATE:20110531
DURATION:P1D
DTSTAMP:20020101T000000Z
RRULE:FREQ=DAILY;COUNT=2
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (DateTime(2011, 6, 1), ),
            ),
            (
                "Recurring with override partial match",
                """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//mulberrymail.com//Mulberry v4.0//EN
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
DTSTART:20110531T120000
DURATION:P1D
DTSTAMP:20020101T000000Z
RRULE:FREQ=DAILY;COUNT=2
SUMMARY:New Year's Day
END:VEVENT
BEGIN:VEVENT
UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
RECURRENCE-ID;VALUE=DATE:20110601T120000
DTSTART;VALUE=DATE:20110601T130000
DURATION:P1D
DTSTAMP:20020101T000000Z
SUMMARY:New Year's Day
END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n"),
                (DateTime(2011, 6, 1, 13, 0, 0), ),
            ),
        )

        for title, caldata, result in data:
            calendar = Calendar.parseText(caldata)
            instances = []
            calendar.getVEvents(
                Period(
                    start=DateTime(2011, 6, 1),
                    end=DateTime(2011, 7, 1),
                ), instances)
            instances = tuple(
                [instance.getInstanceStart() for instance in instances])
            self.assertEqual(
                instances, result, "Failed in %s: got %s, expected %s" %
                (title, instances, result))
Esempio n. 24
0
    def processEventFreeBusy(self, calendar, fbinfo, tzinfo):
        """
        Extract free busy data from a VEVENT component.
        @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
        @param fbinfo: the tuple used to store the three types of fb data.
        @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
        """

        # Expand out the set of instances for the event with in the required range
        instances = calendar.expandTimeRanges(self.timerange.getEnd(), lowerLimit=self.timerange.getStart(), ignoreInvalidInstances=True)

        # Can only do timed events
        for key in instances:
            instance = instances[key]
            if instance.start.isDateOnly():
                return
            break
        else:
            return

        for key in instances:
            instance = instances[key]

            # Apply a timezone to any floating times
            fbstart = instance.start
            if fbstart.floating():
                fbstart.setTimezone(tzinfo)
            fbend = instance.end
            if fbend.floating():
                fbend.setTimezone(tzinfo)

            # Check TRANSP property of underlying component
            if instance.component.hasProperty("TRANSP"):
                # If its TRANSPARENT we always ignore it
                if instance.component.propertyValue("TRANSP") == "TRANSPARENT":
                    continue

            # Determine status
            if instance.component.hasProperty("STATUS"):
                status = instance.component.propertyValue("STATUS")
            else:
                status = "CONFIRMED"

            # Ignore cancelled
            if status == "CANCELLED":
                continue

            # Clip period for this instance - use duration for period end if that
            # is what original component used
            if instance.component.hasProperty("DURATION"):
                period = Period(fbstart, duration=fbend - fbstart)
            else:
                period = Period(fbstart, fbend)
            clipped = clipPeriod(period, self.timerange)

            # Double check for overlap
            if clipped:
                if status == "TENTATIVE":
                    fbinfo.tentative.append(clipped)
                else:
                    fbinfo.busy.append(clipped)
Esempio n. 25
0
    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,))
Esempio n. 26
0
    def _internalGenerateFreeBusyInfo(
        self,
        fbset,
        fbinfo,
        matchtotal,
    ):
        """
        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 matchtotal:  the running total for the number of matches.
        """

        yield self.checkRichOptions(fbset[0]._txn)

        calidmap = dict([(fbcalendar.id(), fbcalendar,) for fbcalendar in fbset])
        directoryService = fbset[0].directoryService()

        results = yield self._matchResources(fbset)

        if self.accountingItems is not None:
            self.accountingItems["fb-resources"] = {}
            for calid, result in results.items():
                aggregated_resources, tzinfo, filter = result
                for k, v in aggregated_resources.items():
                    name, uid, comptype, test_organizer = k
                    self.accountingItems["fb-resources"][uid] = []
                    for float, start, end, fbtype in v:
                        fbstart = tupleToDateTime(start, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        fbend = tupleToDateTime(end, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        self.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 calid, result in results.items():
            calresource = calidmap[calid]
            aggregated_resources, tzinfo, filter = result
            for key in aggregated_resources.iterkeys():

                name, uid, comptype, test_organizer = key

                # Short-cut - if an fbtype exists we can use that
                if comptype == "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

                        # Apply a timezone to any floating times
                        fbstart = tupleToDateTime(start, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)
                        fbend = tupleToDateTime(end, withTimezone=tzinfo if float == 'Y' else Timezone.UTCTimezone)

                        # Clip instance to time range
                        clipped = clipPeriod(Period(fbstart, end=fbend), self.timerange)

                        # Double check for overlap
                        if clipped:
                            # Ignore ones of this UID
                            if not (yield self._testIgnoreExcludeUID(uid, test_organizer, recordUIDCache, directoryService)):
                                clipped.setUseDuration(True)
                                matchedResource = True
                                getattr(fbinfo, self.FBInfo_index_mapper.get(fbtype, "busy")).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 any(self.rich_options.values()):
                            child = (yield calresource.calendarObjectWithName(name))
                            # Only add fully public events
                            if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                                calendar = (yield child.componentForUser())
                                self._addEventDetails(calendar, self.rich_options, 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 {name} is missing from calendar collection {coll!r}", name=name, coll=calresource)
                        continue

                    if self.accountingItems is not None:
                        self.accountingItems.setdefault("fb-filter-match", []).append(uid)

                    if filter.match(calendar, None):

                        # Ignore ones of this UID
                        if (yield self._testIgnoreExcludeUID(uid, calendar.getOrganizer(), recordUIDCache, calresource.directoryService())):
                            continue

                        if self.accountingItems is not None:
                            self.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":
                            self.processEventFreeBusy(calendar, fbinfo, tzinfo)
                        elif calendar.mainType() == "VFREEBUSY":
                            self.processFreeBusyFreeBusy(calendar, fbinfo)
                        elif calendar.mainType() == "VAVAILABILITY":
                            self.processAvailabilityFreeBusy(calendar, fbinfo)
                        else:
                            assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)

                        # Add extended details
                        if calendar.mainType() == "VEVENT" and any(self.rich_options.values()):
                            # Only add fully public events
                            if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                                self._addEventDetails(calendar, self.rich_options, tzinfo)

        returnValue(matchtotal)
Esempio n. 27
0
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)
Esempio n. 28
0
    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)
Esempio n. 29
0
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)
Esempio n. 30
0
    def cacheBusyTime(self):

        # Clear out any existing cache
        self.mBusyTime = []

        # Get all FREEBUSY items and add those that are BUSY
        min_start = DateTime()
        max_end = DateTime()
        props = self.getProperties()
        result = props.get(definitions.cICalProperty_FREEBUSY, ())
        for iter in result:

            # Check the properties FBTYPE parameter
            type = 0
            is_busy = False
            if iter.hasParameter(definitions.cICalParameter_FBTYPE):

                fbyype = iter.getParameterValue(
                    definitions.cICalParameter_FBTYPE)
                if fbyype.upper() == definitions.cICalParameter_FBTYPE_BUSY:

                    is_busy = True
                    type = FreeBusy.BUSY

                elif fbyype.upper(
                ) == definitions.cICalParameter_FBTYPE_BUSYUNAVAILABLE:

                    is_busy = True
                    type = FreeBusy.BUSYUNAVAILABLE

                elif fbyype.upper(
                ) == definitions.cICalParameter_FBTYPE_BUSYTENTATIVE:

                    is_busy = True
                    type = FreeBusy.BUSYTENTATIVE

                else:

                    is_busy = False
                    type = FreeBusy.FREE

            else:

                # Default is busy when no parameter
                is_busy = True
                type = FreeBusy.BUSY

            # Add this period
            if is_busy:

                multi = iter.getMultiValue()
                if (multi is not None) and (multi.getType()
                                            == Value.VALUETYPE_PERIOD):

                    for o in multi.getValues():

                        # Double-check type
                        period = None
                        if isinstance(o, PeriodValue):
                            period = o

                        # Double-check type
                        if period is not None:

                            self.mBusyTime.append(
                                FreeBusy(type, period.getValue()))

                            if len(self.mBusyTime) == 1:

                                min_start = period.getValue().getStart()
                                max_end = period.getValue().getEnd()

                            else:

                                if min_start > period.getValue().getStart():
                                    min_start = period.getValue().getStart()
                                if max_end < period.getValue().getEnd():
                                    max_end = period.getValue().getEnd()

        # If nothing present, empty the list
        if len(self.mBusyTime) == 0:

            self.mBusyTime = None

        else:

            # Sort the list by period
            self.mBusyTime.sort(cmp=lambda x, y: x.getPeriod().getStart().
                                compareDateTime(y.getPeriod().getStart()))

            # Determine range
            start = DateTime()
            end = DateTime()
            if self.mHasStart:
                start = self.mStart
            else:
                start = min_start
            if self.mHasEnd:
                end = self.mEnd
            else:
                end = max_end

            self.mSpanPeriod = Period(start, end)

        self.mCachedBusyTime = True