Пример #1
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
Пример #2
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
Пример #3
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)
Пример #4
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
Пример #5
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)
Пример #6
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))
Пример #7
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")
Пример #8
0
    def test_one_event(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 = [[], [], [], ]
        matchtotal = 0
        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
        self.assertEqual(result, 1)
        self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
Пример #9
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)
Пример #10
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
Пример #11
0
    def test_one_event(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)

        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, 1)
        self.assertEqual(fbinfo.busy, [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
        self.assertEqual(len(fbinfo.tentative), 0)
        self.assertEqual(len(fbinfo.unavailable), 0)
Пример #12
0
    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)
Пример #13
0
    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")),
            ],
        )
Пример #14
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 ()
Пример #15
0
def _externalGenerateFreeBusyInfo(
    calresource,
    fbinfo,
    timerange,
    matchtotal,
    excludeuid=None,
    organizer=None,
    organizerPrincipal=None,
    same_calendar_user=False,
    servertoserver=False,
    event_details=None,
    logItems=None,
    accountingItems=None,
):
    """
    Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
    any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.

    See L{_internalGenerateFreeBusyInfo} for argument description.
    """
    fbresults, matchtotal = yield calresource._txn.store(
    ).conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid,
                            organizer, organizerPrincipal, same_calendar_user,
                            servertoserver, event_details)
    for i in range(3):
        fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
    returnValue(matchtotal)
Пример #16
0
    def test_one_event(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 = [[], [], [], ]
        matchtotal = 0
        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
        self.assertEqual(result, 1)
        self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
        self.assertEqual(len(fbinfo[1]), 0)
        self.assertEqual(len(fbinfo[2]), 0)
Пример #17
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]))
Пример #18
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
Пример #19
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),)
        ))
Пример #20
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]
Пример #21
0
def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
    """
    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.
    @param timerange: the time range to restrict free busy data to.
    """

    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(utc=True))
        end = vav.getEndDateUTC()
        if end is None:
            end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
        period = Period(start, end)
        overall = clipPeriod(period, Period(timerange.start, timerange.end))
        if overall is None:
            continue

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

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

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

        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
Пример #22
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")),
            ],
        )
Пример #23
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
Пример #24
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")
Пример #25
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)
Пример #26
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))))
Пример #27
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))
Пример #28
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
Пример #29
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)
Пример #30
0
    def recv_freebusy(self, txn, request):
        """
        Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}.

        @param request: request arguments
        @type request: C{dict}
        """

        # Operate on the L{CommonHomeChild}
        calresource, _ignore = yield self._getStoreObjectForRequest(
            txn, request)

        organizer = yield calendarUserFromCalendarUserAddress(
            request["organizer"], txn) if request["organizer"] else None
        recipient = yield calendarUserFromCalendarUserAddress(
            request["recipient"], txn) if request["recipient"] else None

        freebusy = FreebusyQuery(
            organizer=organizer,
            recipient=recipient,
            timerange=Period.parseText(request["timerange"]),
            excludeUID=request["excludeuid"],
            event_details=request["event_details"],
        )
        fbinfo = FreebusyQuery.FBInfo([], [], [])
        matchtotal = yield freebusy.generateFreeBusyInfo(
            [
                calresource,
            ],
            fbinfo,
            matchtotal=request["matchtotal"],
        )

        # Convert L{Period} objects to text for JSON response
        returnValue({
            "fbresults": [
                [item.getText() for item in fbinfo.busy],
                [item.getText() for item in fbinfo.tentative],
                [item.getText() for item in fbinfo.unavailable],
            ],
            "matchtotal":
            matchtotal,
        })
Пример #31
0
    def _externalGenerateFreeBusyInfo(self, fbset, fbinfo, matchtotal):
        """
        Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
        any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.

        See L{_internalGenerateFreeBusyInfo} for argument description.
        """
        for calresource in fbset:
            fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(
                calresource,
                self.organizer.cuaddr if self.organizer else None,
                self.recipient.cuaddr if self.recipient else None,
                self.timerange,
                matchtotal,
                self.excludeuid,
                self.event_details,
            )
            for i in range(3):
                fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
        returnValue(matchtotal)
Пример #32
0
    def _externalGenerateFreeBusyInfo(self, fbset, fbinfo, matchtotal):
        """
        Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
        any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.

        See L{_internalGenerateFreeBusyInfo} for argument description.
        """
        for calresource in fbset:
            fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(
                calresource,
                self.organizer.cuaddr if self.organizer else None,
                self.recipient.cuaddr if self.recipient else None,
                self.timerange,
                matchtotal,
                self.excludeuid,
                self.event_details,
            )
            for i in range(3):
                fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
        returnValue(matchtotal)
Пример #33
0
    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)
Пример #34
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"]))
Пример #35
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)
Пример #36
0
def _externalGenerateFreeBusyInfo(
    calresource,
    fbinfo,
    timerange,
    matchtotal,
    excludeuid=None,
    organizer=None,
    organizerPrincipal=None,
    same_calendar_user=False,
    servertoserver=False,
    event_details=None,
    logItems=None,
):
    """
    Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
    any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.

    See L{_internalGenerateFreeBusyInfo} for argument description.
    """
    fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid, organizer, organizerPrincipal, same_calendar_user, servertoserver, event_details)
    for i in range(3):
        fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
    returnValue(matchtotal)
Пример #37
0
    def recv_freebusy(self, txn, request):
        """
        Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}.

        @param request: request arguments
        @type request: C{dict}
        """

        # Operate on the L{CommonHomeChild}
        calresource, _ignore = yield self._getStoreObjectForRequest(txn, request)

        organizer = yield calendarUserFromCalendarUserAddress(request["organizer"], txn) if request["organizer"] else None
        recipient = yield calendarUserFromCalendarUserAddress(request["recipient"], txn) if request["recipient"] else None

        freebusy = FreebusyQuery(
            organizer=organizer,
            recipient=recipient,
            timerange=Period.parseText(request["timerange"]),
            excludeUID=request["excludeuid"],
            event_details=request["event_details"],
        )
        fbinfo = FreebusyQuery.FBInfo([], [], [])
        matchtotal = yield freebusy.generateFreeBusyInfo(
            [calresource, ],
            fbinfo,
            matchtotal=request["matchtotal"],
        )

        # Convert L{Period} objects to text for JSON response
        returnValue({
            "fbresults": [
                [item.getText() for item in fbinfo.busy],
                [item.getText() for item in fbinfo.tentative],
                [item.getText() for item in fbinfo.unavailable],
            ],
            "matchtotal": matchtotal,
        })
Пример #38
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)
Пример #39
0
    def test_freebusy(self):
        """
        Test that action=component works.
        """

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

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

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

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

        fbinfo = [[], [], []]
        matchtotal = yield generateFreeBusyInfo(
            shared,
            fbinfo,
            TimeRange(start=fbstart, end=fbend),
            0,
            excludeuid=None,
            organizer=None,
            organizerPrincipal=None,
            same_calendar_user=False,
            servertoserver=False,
            event_details=False,
            logItems=None
        )

        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.otherCommit()
Пример #40
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"
Пример #41
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)
Пример #42
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,))
Пример #43
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)
Пример #44
0
    def test_simple(self):

        data = (
            (
                "#1.1 No busy time",
                [
                    [],
                    [],
                    [],
                ],
                "20080601T000000Z",
                "20080602T000000Z",
                None,
                None,
                None,
                """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VFREEBUSY
DTSTART:20080601T000000Z
DTEND:20080602T000000Z
END:VFREEBUSY
END:VCALENDAR
""",
            ),
            (
                "#1.2 No busy time with organizer & attendee",
                [
                    [],
                    [],
                    [],
                ],
                "20080601T000000Z",
                "20080602T000000Z",
                Property("ORGANIZER", "mailto:[email protected]"),
                Property("ATTENDEE", "mailto:[email protected]"),
                None,
                """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VFREEBUSY
DTSTART:20080601T000000Z
DTEND:20080602T000000Z
ATTENDEE:mailto:[email protected]
ORGANIZER:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
""",
            ),
            (
                "#1.3 With single busy time",
                [
                    [Period.parseText("20080601T120000Z/20080601T130000Z"), ],
                    [],
                    [],
                ],
                "20080601T000000Z",
                "20080602T000000Z",
                Property("ORGANIZER", "mailto:[email protected]"),
                Property("ATTENDEE", "mailto:[email protected]"),
                None,
                """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VFREEBUSY
DTSTART:20080601T000000Z
DTEND:20080602T000000Z
ATTENDEE:mailto:[email protected]
FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z
ORGANIZER:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
""",
            ),
            (
                "#1.4 With multiple busy time",
                [
                    [
                        Period.parseText("20080601T120000Z/20080601T130000Z"),
                        Period.parseText("20080601T140000Z/20080601T150000Z"),
                    ],
                    [],
                    [],
                ],
                "20080601T000000Z",
                "20080602T000000Z",
                Property("ORGANIZER", "mailto:[email protected]"),
                Property("ATTENDEE", "mailto:[email protected]"),
                None,
                """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VFREEBUSY
DTSTART:20080601T000000Z
DTEND:20080602T000000Z
ATTENDEE:mailto:[email protected]
FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z,20080601T140000Z/20080601T150000Z
ORGANIZER:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
""",
            ),
            (
                "#1.5 With multiple busy time, some overlap",
                [
                    [
                        Period.parseText("20080601T120000Z/20080601T130000Z"),
                        Period.parseText("20080601T123000Z/20080601T133000Z"),
                        Period.parseText("20080601T140000Z/20080601T150000Z"),
                        Period.parseText("20080601T150000Z/20080601T160000Z"),
                    ],
                    [],
                    [],
                ],
                "20080601T000000Z",
                "20080602T000000Z",
                Property("ORGANIZER", "mailto:[email protected]"),
                Property("ATTENDEE", "mailto:[email protected]"),
                None,
                """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VFREEBUSY
DTSTART:20080601T000000Z
DTEND:20080602T000000Z
ATTENDEE:mailto:[email protected]
FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T133000Z,20080601T140000Z/20080601T160000Z
ORGANIZER:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
""",
            ),
            (
                "#1.6 With all busy time types",
                [
                    [
                        Period.parseText("20080601T120000Z/20080601T130000Z"),
                        Period.parseText("20080601T140000Z/20080601T150000Z"),
                    ],
                    [
                        Period.parseText("20080601T140000Z/20080601T150000Z"),
                    ],
                    [
                        Period.parseText("20080601T160000Z/20080601T170000Z"),
                    ],
                ],
                "20080601T000000Z",
                "20080602T000000Z",
                Property("ORGANIZER", "mailto:[email protected]"),
                Property("ATTENDEE", "mailto:[email protected]"),
                None,
                """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VFREEBUSY
DTSTART:20080601T000000Z
DTEND:20080602T000000Z
ATTENDEE:mailto:[email protected]
FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z,20080601T140000Z/20080601T150000Z
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20080601T140000Z/20080601T150000Z
FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20080601T160000Z/20080601T170000Z
ORGANIZER:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
""",
            ),
            (
                "#1.7 With single busy time and event details",
                [
                    [Period.parseText("20080601T120000Z/20080601T130000Z"), ],
                    [],
                    [],
                ],
                "20080601T000000Z",
                "20080602T000000Z",
                Property("ORGANIZER", "mailto:[email protected]"),
                Property("ATTENDEE", "mailto:[email protected]"),
                [
                    tuple(Component.fromString("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:1234-5678
DTSTAMP:20080601T000000Z
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
END:VEVENT
END:VCALENDAR
""").subcomponents())[0],
                ],
                """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
DTSTART:20080601T120000Z
DTEND:20080601T130000Z
END:VEVENT
BEGIN:VFREEBUSY
DTSTART:20080601T000000Z
DTEND:20080602T000000Z
ATTENDEE:mailto:[email protected]
FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z
ORGANIZER:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
""",
            ),
        )

        for description, fbinfo, dtstart, dtend, organizer, attendee, event_details, calendar in data:
            timerange = caldavxml.TimeRange(start=dtstart, end=dtend)
            result = buildFreeBusyResult(fbinfo, timerange, organizer=organizer, attendee=attendee, event_details=event_details)
            self.assertEqual(normalizeiCalendarText(str(result)), calendar.replace("\n", "\r\n"), msg=description)
Пример #45
0
    def testParseGenerate(self):

        for result in TestPeriod.test_data:
            period = Period.parseText(result)
            self.assertEqual(period.getText(), result)