<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14683] CalendarServer/trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14683">14683</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-04-17 11:11:45 -0700 (Fri, 17 Apr 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Refactor freebusy queries and remove a bunch of duplicate code, in preparation for trying to do some optimizations.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunktwistedcaldavfreebusyurlpy">CalendarServer/trunk/twistedcaldav/freebusyurl.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethodreport_commonpy">CalendarServer/trunk/twistedcaldav/method/report_common.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethodreport_freebusypy">CalendarServer/trunk/twistedcaldav/method/report_freebusy.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingcaldavdeliverypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingfreebusypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_freebusypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastorepoddingstore_apipy">CalendarServer/trunk/txdav/common/datastore/podding/store_api.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastorepoddingtesttest_conduitpy">CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunktwistedcaldavfreebusyurlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/freebusyurl.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/freebusyurl.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/freebusyurl.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -26,7 +26,6 @@
</span><span class="cx"> 
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from txweb2 import responsecode
</span><del>-from txdav.xml import element as davxml
</del><span class="cx"> from txweb2.dav.http import ErrorResponse
</span><span class="cx"> from txweb2.dav.util import joinURL
</span><span class="cx"> from txweb2.http import HTTPError
</span><span class="lines">@@ -36,19 +35,19 @@
</span><span class="cx"> from txweb2.stream import MemoryStream
</span><span class="cx"> 
</span><span class="cx"> from twistedcaldav import caldavxml
</span><del>-from twistedcaldav.caldavxml import TimeRange
</del><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.customxml import calendarserver_namespace
</span><span class="cx"> from twistedcaldav.ical import Property
</span><span class="cx"> from twistedcaldav.resource import CalDAVResource, ReadOnlyNoCopyResourceMixIn
</span><span class="cx"> from twistedcaldav.scheduling_store.caldav.resource import deliverSchedulePrivilegeSet
</span><span class="cx"> 
</span><del>-from txdav.caldav.datastore.scheduling.caldav.delivery import ScheduleViaCalDAV
</del><span class="cx"> from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
</span><del>-from txdav.caldav.datastore.scheduling.scheduler import Scheduler
</del><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
+from txdav.xml import element as davxml
</ins><span class="cx"> 
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="cx"> from pycalendar.duration import Duration
</span><ins>+from pycalendar.period import Period
</ins><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -266,26 +265,16 @@
</span><span class="cx">         if inbox is None:
</span><span class="cx">             raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, &quot;No schedule inbox for principal: %s&quot; % (principal,)))
</span><span class="cx"> 
</span><del>-        scheduler = Scheduler(request, self)
-        scheduler.timeRange = TimeRange(start=&quot;20000101T000000Z&quot;, end=&quot;20070102T000000Z&quot;)
-        scheduler.timeRange.start = self.start
-        scheduler.timeRange.end = self.end
</del><ins>+        organizer = recipient = LocalCalendarUser(cuaddr, principal.record)
+        recipient.inbox = inbox._newStoreObject
+        attendeeProp = Property(&quot;ATTENDEE&quot;, recipient.cuaddr)
+        timerange = Period(self.start, self.end)
</ins><span class="cx"> 
</span><del>-        scheduler.organizer = LocalCalendarUser(cuaddr, principal.record)
-        scheduler.organizer.inbox = inbox._newStoreObject
</del><ins>+        fbresult = (yield FreebusyQuery(
+            organizer, None, recipient, attendeeProp, None,
+            timerange, None, None,
+        ).generateAttendeeFreeBusyResponse())
</ins><span class="cx"> 
</span><del>-        attendeeProp = Property(&quot;ATTENDEE&quot;, scheduler.organizer.cuaddr)
-
-        requestor = ScheduleViaCalDAV(scheduler, (), [], True)
-        fbresult = (yield requestor.generateAttendeeFreeBusyResponse(
-            scheduler.organizer,
-            None,
-            None,
-            None,
-            attendeeProp,
-            True,
-        ))
-
</del><span class="cx">         response = Response()
</span><span class="cx">         response.stream = MemoryStream(str(fbresult))
</span><span class="cx">         response.headers.setHeader(&quot;content-type&quot;, MimeType.fromString(&quot;%s; charset=utf-8&quot; % (self.format,)))
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethodreport_commonpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/method/report_common.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -29,55 +29,33 @@
</span><span class="cx">     &quot;buildFreeBusyResult&quot;,
</span><span class="cx"> ]
</span><span class="cx"> 
</span><del>-import time
-
-try:
-    from hashlib import md5
-except ImportError:
-    from md5 import new as md5
-
</del><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from twisted.python.failure import Failure
</span><span class="cx"> 
</span><span class="cx"> from txweb2 import responsecode
</span><span class="cx"> from txweb2.dav.http import statusForFailure
</span><span class="cx"> from txweb2.dav.method.propfind import propertyName
</span><del>-from txweb2.dav.method.report import NumberOfMatchesWithinLimits
-from txweb2.dav.method.report import max_number_of_matches
</del><span class="cx"> from txweb2.dav.resource import AccessDeniedError
</span><del>-from txweb2.http import HTTPError, StatusResponse
</del><ins>+from txweb2.http import HTTPError
</ins><span class="cx"> 
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> 
</span><span class="cx"> from twistedcaldav import caldavxml
</span><span class="cx"> from twistedcaldav import carddavxml
</span><del>-from twistedcaldav.caldavxml import CalendarData, CalendarTimeZone, TimeRange
</del><ins>+from twistedcaldav.caldavxml import CalendarData
</ins><span class="cx"> from twistedcaldav.carddavxml import AddressData
</span><del>-from twistedcaldav.config import config
</del><span class="cx"> from twistedcaldav.datafilters.calendardata import CalendarDataFilter
</span><span class="cx"> from twistedcaldav.datafilters.hiddeninstance import HiddenInstanceFilter
</span><span class="cx"> from twistedcaldav.datafilters.privateevents import PrivateEventFilter
</span><span class="cx"> from twistedcaldav.datafilters.addressdata import AddressDataFilter
</span><del>-from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap, \
-    compareDateTime, normalizeToUTC, parseSQLTimestampToPyCalendar
-from twistedcaldav.ical import Component, Property, iCalendarProductID
-from twistedcaldav.instance import InstanceList
-from twistedcaldav.memcacher import Memcacher
</del><span class="cx"> 
</span><del>-from txdav.caldav.datastore.query.filter import Filter
-from txdav.common.icommondatastore import IndexedSearchException
</del><span class="cx"> from txdav.xml import element
</span><span class="cx"> 
</span><del>-from pycalendar.duration import Duration
-from pycalendar.datetime import DateTime
-from pycalendar.timezone import Timezone
-from pycalendar.period import Period
-
</del><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="cx"> COLLECTION_TYPE_REGULAR = &quot;collection&quot;
</span><span class="cx"> COLLECTION_TYPE_CALENDAR = &quot;calendar&quot;
</span><del>-COLLECTION_TYPE_ADDRESSBOOK = &quot;adressbook&quot;
</del><ins>+COLLECTION_TYPE_ADDRESSBOOK = &quot;addressbook&quot;
</ins><span class="cx"> 
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def applyToCalendarCollections(resource, request, request_uri, depth, apply, privileges):
</span><span class="lines">@@ -422,607 +400,3 @@
</span><span class="cx">             properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
</span><span class="cx"> 
</span><span class="cx">     returnValue(properties_by_status)
</span><del>-
-fbtype_mapper = {&quot;BUSY&quot;: 0, &quot;BUSY-TENTATIVE&quot;: 1, &quot;BUSY-UNAVAILABLE&quot;: 2}
-fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
-
-fbcacher = Memcacher(&quot;FBCache&quot;, pickle=True)
-
-class FBCacheEntry(object):
-
-    CACHE_DAYS_FLOATING_ADJUST = 1
-
-    def __init__(self, key, token, timerange, fbresults):
-        self.key = key
-        self.token = token
-        self.timerange = timerange
-        self.fbresults = fbresults
-
-
-    @classmethod
-    @inlineCallbacks
-    def getCacheEntry(cls, calresource, useruid, timerange):
-
-        key = calresource.resourceID() + &quot;/&quot; + useruid
-        token = (yield calresource.getInternalSyncToken())
-        entry = (yield fbcacher.get(key))
-
-        if entry:
-
-            # Offset one day at either end to account for floating
-            cached_start = entry.timerange.start + Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
-            cached_end = entry.timerange.end - Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
-
-            # Verify that the requested timerange lies within the cache timerange
-            if compareDateTime(timerange.end, cached_end) &lt;= 0 and compareDateTime(timerange.start, cached_start) &gt;= 0:
-
-                # Verify that cached entry is still valid
-                if token == entry.token:
-                    returnValue(entry.fbresults)
-
-        returnValue(None)
-
-
-    @classmethod
-    @inlineCallbacks
-    def makeCacheEntry(cls, calresource, useruid, timerange, fbresults):
-
-        key = calresource.resourceID() + &quot;/&quot; + useruid
-        token = (yield calresource.getInternalSyncToken())
-        entry = cls(key, token, timerange, fbresults)
-        yield fbcacher.set(key, entry)
-
-
-
-@inlineCallbacks
-def generateFreeBusyInfo(
-    request,
-    calresource,
-    fbinfo,
-    timerange,
-    matchtotal,
-    excludeuid=None,
-    organizer=None,
-    organizerPrincipal=None,
-    same_calendar_user=False,
-    servertoserver=False,
-    event_details=None,
-):
-    &quot;&quot;&quot;
-    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}
-    &quot;&quot;&quot;
-
-    # 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 &quot;&quot;
-
-    # Free busy is per-user
-    userPrincipal = (yield calresource.resourceOwnerPrincipal(request))
-    if userPrincipal:
-        useruid = userPrincipal.principalUID()
-    else:
-        useruid = &quot;&quot;
-
-    # 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 = {
-        &quot;organizer&quot;: False,
-        &quot;delegate&quot;: False,
-        &quot;resource&quot;: 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[&quot;organizer&quot;] = 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[&quot;delegate&quot;] = True
-
-        # Check if attendee is room or resource
-        if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType() in (&quot;RESOURCE&quot;, &quot;ROOM&quot;,):
-            do_event_details = True
-            rich_options[&quot;resource&quot;] = 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, &quot;extendedLogItems&quot;):
-                request.extendedLogItems = {}
-            request.extendedLogItems[&quot;fb-uncached&quot;] = request.extendedLogItems.get(&quot;fb-uncached&quot;, 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) &gt;= 0 and compareDateTime(timerange.end, cache_end) &lt;= 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=(&quot;VEVENT&quot;, &quot;VFREEBUSY&quot;, &quot;VAVAILABILITY&quot;),
-                ),
-                name=&quot;VCALENDAR&quot;,
-            )
-        )
-        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,
-                &quot;Failed freebusy query&quot;
-            ))
-
-    else:
-        # Log extended item
-        if not hasattr(request, &quot;extendedLogItems&quot;):
-            request.extendedLogItems = {}
-        request.extendedLogItems[&quot;fb-cached&quot;] = request.extendedLogItems.get(&quot;fb-cached&quot;, 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 == &quot;VEVENT&quot; 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 &quot;&quot;
-
-                        # 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 == &quot;&quot;) 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 &gt; 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.componentForUser())
-                    _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
-
-        else:
-            child = (yield request.locateChildResource(calresource, 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(&quot;Calendar %s is missing from calendar collection %r&quot; % (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 &quot;&quot;
-
-                    # 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 &gt; max_number_of_matches:
-                    raise NumberOfMatchesWithinLimits(max_number_of_matches)
-
-                if calendar.mainType() == &quot;VEVENT&quot;:
-                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
-                elif calendar.mainType() == &quot;VFREEBUSY&quot;:
-                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
-                elif calendar.mainType() == &quot;VAVAILABILITY&quot;:
-                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
-                else:
-                    assert &quot;Free-busy query returned unwanted component: %s in %r&quot;, (name, calresource,)
-
-                # Add extended details
-                if calendar.mainType() == &quot;VEVENT&quot; and do_event_details:
-                    child = (yield request.locateChildResource(calresource, name))
-                    calendar = (yield child.componentForUser())
-                    _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
-
-    returnValue(matchtotal)
-
-
-
-def _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo):
-    &quot;&quot;&quot;
-    Expand events within the specified time range and limit the set of properties to those allowed for
-    delegate extended free busy.
-
-    @param calendar: the calendar object to expand
-    @type calendar: L{Component}
-    @param event_details: list to append VEVENT components to
-    @type event_details: C{list}
-    @param timerange: the time-range in which to expand
-    @type timerange: L{TimeRange}
-    @param tzinfo: timezone for floating time calculations
-    @type tzinfo: L{Timezone}
-    &quot;&quot;&quot;
-
-    # First expand the component
-    expanded = calendar.expand(timerange.start, timerange.end, timezone=tzinfo)
-
-    keep_props = (
-        &quot;UID&quot;,
-        &quot;RECURRENCE-ID&quot;,
-        &quot;DTSTAMP&quot;,
-        &quot;DTSTART&quot;,
-        &quot;DTEND&quot;,
-        &quot;DURATION&quot;,
-    )
-
-    if rich_options[&quot;organizer&quot;] or rich_options[&quot;delegate&quot;]:
-        keep_props += (&quot;SUMMARY&quot;,)
-
-    if rich_options[&quot;organizer&quot;] or rich_options[&quot;resource&quot;]:
-        keep_props += (&quot;ORGANIZER&quot;,)
-
-    # Remove all but essential properties
-    expanded.filterProperties(keep=keep_props)
-
-    # Need to remove all child components of VEVENT
-    for subcomponent in expanded.subcomponents():
-        if subcomponent.name() == &quot;VEVENT&quot;:
-            for sub in tuple(subcomponent.subcomponents()):
-                subcomponent.removeComponent(sub)
-
-    event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == &quot;VEVENT&quot;])
-
-
-
-def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
-    &quot;&quot;&quot;
-    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 timerange: the time range to restrict free busy data to.
-    @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
-    &quot;&quot;&quot;
-
-    # Expand out the set of instances for the event with in the required range
-    instances = calendar.expandTimeRanges(timerange.end, lowerLimit=timerange.start, 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(&quot;TRANSP&quot;):
-            # If its TRANSPARENT we always ignore it
-            if instance.component.propertyValue(&quot;TRANSP&quot;) == &quot;TRANSPARENT&quot;:
-                continue
-
-        # Determine status
-        if instance.component.hasProperty(&quot;STATUS&quot;):
-            status = instance.component.propertyValue(&quot;STATUS&quot;)
-        else:
-            status = &quot;CONFIRMED&quot;
-
-        # Ignore cancelled
-        if status == &quot;CANCELLED&quot;:
-            continue
-
-        # Clip period for this instance - use duration for period end if that
-        # is what original component used
-        if instance.component.hasProperty(&quot;DURATION&quot;):
-            period = Period(fbstart, duration=fbend - fbstart)
-        else:
-            period = Period(fbstart, fbend)
-        clipped = clipPeriod(period, Period(timerange.start, timerange.end))
-
-        # Double check for overlap
-        if clipped:
-            if status == &quot;TENTATIVE&quot;:
-                fbinfo[1].append(clipped)
-            else:
-                fbinfo[0].append(clipped)
-
-
-
-def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
-    &quot;&quot;&quot;
-    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.
-    &quot;&quot;&quot;
-
-    for vfb in [x for x in calendar.subcomponents() if x.name() == &quot;VFREEBUSY&quot;]:
-        # 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(&quot;FREEBUSY&quot;):
-            # Check the type
-            fbtype = fb.parameterValue(&quot;FBTYPE&quot;, default=&quot;BUSY&quot;)
-            if fbtype == &quot;FREE&quot;:
-                continue
-
-            # Look at each period in the property
-            assert isinstance(fb.value(), list), &quot;FREEBUSY property does not contain a list of values: %r&quot; % (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)
-
-
-
-def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
-    &quot;&quot;&quot;
-    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.
-    &quot;&quot;&quot;
-
-    for vav in [x for x in calendar.subcomponents() if x.name() == &quot;VAVAILABILITY&quot;]:
-
-        # 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 &lt; period.getStart():
-                busyperiods.append(Period(last_end, period.getStart()))
-            last_end = period.getEnd()
-        if last_end &lt; timerange.end:
-            busyperiods.append(Period(last_end, timerange.end))
-
-        # Add to actual results mapped by busy type
-        fbtype = vav.propertyValue(&quot;BUSYTYPE&quot;)
-        if fbtype is None:
-            fbtype = &quot;BUSY-UNAVAILABLE&quot;
-
-        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
-
-
-
-def processAvailablePeriods(calendar, timerange):
-    &quot;&quot;&quot;
-    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.
-    &quot;&quot;&quot;
-
-    periods = []
-
-    # First we need to group all AVAILABLE sub-components by UID
-    uidmap = {}
-    for component in calendar.subcomponents():
-        if component.name() == &quot;AVAILABLE&quot;:
-            uid = component.propertyValue(&quot;UID&quot;)
-            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(&quot;DURATION&quot;):
-                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
-
-
-
-def buildFreeBusyResult(fbinfo, timerange, organizer=None, attendee=None, uid=None, method=None, event_details=None):
-    &quot;&quot;&quot;
-    Generate a VCALENDAR object containing a single VFREEBUSY that is the
-    aggregate of the free busy info passed in.
-
-    @param fbinfo:        the array of busy periods to use.
-    @param timerange:     the L{TimeRange} for the query.
-    @param organizer:     the L{Property} for the Organizer of the free busy request, or None.
-    @param attendee:      the L{Property} for the Attendee responding to the free busy request, or None.
-    @param uid:           the UID value from the free busy request.
-    @param method:        the METHOD property value to insert.
-    @param event_details: VEVENT components to add.
-    @return:              the L{Component} containing the calendar data.
-    &quot;&quot;&quot;
-
-    # Merge overlapping time ranges in each fb info section
-    normalizePeriodList(fbinfo[0])
-    normalizePeriodList(fbinfo[1])
-    normalizePeriodList(fbinfo[2])
-
-    # Now build a new calendar object with the free busy info we have
-    fbcalendar = Component(&quot;VCALENDAR&quot;)
-    fbcalendar.addProperty(Property(&quot;VERSION&quot;, &quot;2.0&quot;))
-    fbcalendar.addProperty(Property(&quot;PRODID&quot;, iCalendarProductID))
-    if method:
-        fbcalendar.addProperty(Property(&quot;METHOD&quot;, method))
-    fb = Component(&quot;VFREEBUSY&quot;)
-    fbcalendar.addComponent(fb)
-    if organizer is not None:
-        fb.addProperty(organizer)
-    if attendee is not None:
-        fb.addProperty(attendee)
-    fb.addProperty(Property(&quot;DTSTART&quot;, timerange.start))
-    fb.addProperty(Property(&quot;DTEND&quot;, timerange.end))
-    fb.addProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-    if len(fbinfo[0]) != 0:
-        fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo[0], {&quot;FBTYPE&quot;: &quot;BUSY&quot;}))
-    if len(fbinfo[1]) != 0:
-        fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo[1], {&quot;FBTYPE&quot;: &quot;BUSY-TENTATIVE&quot;}))
-    if len(fbinfo[2]) != 0:
-        fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo[2], {&quot;FBTYPE&quot;: &quot;BUSY-UNAVAILABLE&quot;}))
-    if uid is not None:
-        fb.addProperty(Property(&quot;UID&quot;, uid))
-    else:
-        uid = md5(str(fbcalendar) + str(time.time())).hexdigest()
-        fb.addProperty(Property(&quot;UID&quot;, uid))
-
-    if event_details:
-        for vevent in event_details:
-            fbcalendar.addComponent(vevent)
-
-    return fbcalendar
</del></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethodreport_freebusypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/method/report_freebusy.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/method/report_freebusy.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/method/report_freebusy.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -20,9 +20,10 @@
</span><span class="cx"> 
</span><span class="cx"> __all__ = [&quot;report_urn_ietf_params_xml_ns_caldav_free_busy_query&quot;]
</span><span class="cx"> 
</span><del>-from twisted.internet.defer import inlineCallbacks, returnValue
</del><ins>+from twext.python.log import Logger
</ins><span class="cx"> 
</span><del>-from twext.python.log import Logger
</del><ins>+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
</ins><span class="cx"> from txweb2 import responsecode
</span><span class="cx"> from txweb2.dav.http import ErrorResponse
</span><span class="cx"> from txweb2.dav.method.report import NumberOfMatchesWithinLimits
</span><span class="lines">@@ -36,12 +37,15 @@
</span><span class="cx"> from twistedcaldav.util import bestAcceptType
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
</span><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.xml import element as davxml
</span><span class="cx"> 
</span><ins>+from pycalendar.period import Period
+
</ins><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><span class="cx"> @inlineCallbacks
</span><del>-def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): #@UnusedVariable
</del><ins>+def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy):
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Generate a free-busy REPORT.
</span><span class="cx">     (CalDAV-access-09, section 7.8)
</span><span class="lines">@@ -57,36 +61,36 @@
</span><span class="cx">     if not timerange.valid():
</span><span class="cx">         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, &quot;Invalid time-range specified&quot;))
</span><span class="cx"> 
</span><del>-    # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-    fbinfo = ([], [], [])
</del><ins>+    fbset = []
</ins><span class="cx"> 
</span><del>-    matchcount = [0]
-
</del><span class="cx">     accepted_type = bestAcceptType(request.headers.getHeader(&quot;accept&quot;), Component.allowedTypes())
</span><span class="cx">     if accepted_type is None:
</span><span class="cx">         raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, &quot;Cannot generate requested data type&quot;))
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def generateFreeBusyInfo(calresource, uri): #@UnusedVariable
</del><ins>+    def getCalendarList(calresource, uri): #@UnusedVariable
</ins><span class="cx">         &quot;&quot;&quot;
</span><del>-        Run a free busy report on the specified calendar collection
-        accumulating the free busy info for later processing.
</del><ins>+        Store the calendars that match the query in L{fbset} which will then be used with the
+        freebusy query.
+
</ins><span class="cx">         @param calresource: the L{CalDAVResource} for a calendar collection.
</span><del>-        @param uri: the uri for the calendar collecton resource.
</del><ins>+        @param uri: the uri for the calendar collection resource.
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        def _gotResult(result):
-            matchcount[0] = result
-            return True
</del><ins>+        fbset.append(calresource._newStoreObject)
+        return succeed(True)
</ins><span class="cx"> 
</span><del>-        d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0])
-        d.addCallback(_gotResult)
-        return d
</del><ins>+    # Run report taking depth into account
+    depth = request.headers.getHeader(&quot;depth&quot;, &quot;0&quot;)
+    yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),))
</ins><span class="cx"> 
</span><del>-    # Run report taking depth into account
</del><ins>+    # Do the actual freebusy query against the set of matched calendars
+    timerange = Period(timerange.start, timerange.end)
</ins><span class="cx">     try:
</span><del>-        depth = request.headers.getHeader(&quot;depth&quot;, &quot;0&quot;)
-        yield report_common.applyToCalendarCollections(self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(),))
</del><ins>+        fbresult = (yield FreebusyQuery(
+            None, None, None, None, None,
+            timerange, None, None,
+        ).generateAttendeeFreeBusyResponse(fbset=fbset, method=None))
</ins><span class="cx">     except NumberOfMatchesWithinLimits:
</span><span class="cx">         log.error(&quot;Too many matching components in free-busy report&quot;)
</span><span class="cx">         raise HTTPError(ErrorResponse(
</span><span class="lines">@@ -107,11 +111,8 @@
</span><span class="cx">             &quot;Time-range value too far in the future. Must be on or before %s.&quot; % (str(e.limit),)
</span><span class="cx">         ))
</span><span class="cx"> 
</span><del>-    # Now build a new calendar object with the free busy info we have
-    fbcalendar = report_common.buildFreeBusyResult(fbinfo, timerange)
-
</del><span class="cx">     response = Response()
</span><del>-    response.stream = MemoryStream(fbcalendar.getText(accepted_type))
</del><ins>+    response.stream = MemoryStream(fbresult.getText(accepted_type))
</ins><span class="cx">     response.headers.setHeader(&quot;content-type&quot;, MimeType.fromString(&quot;%s; charset=utf-8&quot; % (accepted_type,)))
</span><span class="cx"> 
</span><span class="cx">     returnValue(response)
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingcaldavdeliverypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -26,10 +26,8 @@
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> 
</span><span class="cx"> from txdav.base.propertystore.base import PropertyName
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser, OtherServerCalendarUser
</del><span class="cx"> from txdav.caldav.datastore.scheduling.delivery import DeliveryService
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import processAvailabilityFreeBusy, \
-    generateFreeBusyInfo, buildFreeBusyResult
</del><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
</span><span class="cx"> from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
</span><span class="cx"> from txdav.caldav.datastore.scheduling.utils import extractEmailDomain
</span><span class="lines">@@ -97,10 +95,6 @@
</span><span class="cx">         organizerProp = self.scheduler.calendar.getOrganizerProperty()
</span><span class="cx">         uid = self.scheduler.calendar.resourceUID()
</span><span class="cx"> 
</span><del>-        organizerPrincipal = None
-        if type(self.scheduler.organizer) in (LocalCalendarUser, OtherServerCalendarUser,):
-            organizerPrincipal = self.scheduler.organizer.record.uid
-
</del><span class="cx">         for recipient in self.recipients:
</span><span class="cx"> 
</span><span class="cx">             #
</span><span class="lines">@@ -113,7 +107,7 @@
</span><span class="cx">                 # Look for special delegate extended free-busy request
</span><span class="cx">                 event_details = [] if self.scheduler.calendar.getExtendedFreeBusy() else None
</span><span class="cx"> 
</span><del>-                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid, event_details)
</del><ins>+                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, uid, event_details)
</ins><span class="cx">             else:
</span><span class="cx">                 yield self.generateResponse(recipient, self.responses)
</span><span class="cx"> 
</span><span class="lines">@@ -199,32 +193,26 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details):
</del><ins>+    def generateFreeBusyResponse(self, recipient, responses, organizerProp, uid, event_details):
</ins><span class="cx"> 
</span><span class="cx">         # Extract the ATTENDEE property matching current recipient from the calendar data
</span><span class="cx">         cuas = recipient.record.calendarUserAddresses
</span><span class="cx">         attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
</span><span class="cx"> 
</span><del>-        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
-
</del><span class="cx">         try:
</span><del>-            fbresult = (yield self.generateAttendeeFreeBusyResponse(
-                recipient,
-                organizerProp,
-                organizerPrincipal,
-                uid,
-                attendeeProp,
-                remote,
-                event_details,
-            ))
-        except Exception:
</del><ins>+            fbresult = (yield FreebusyQuery(
+                self.scheduler.organizer, organizerProp, recipient, attendeeProp, uid,
+                self.scheduler.timeRange, self.scheduler.excludeUID, self.scheduler.logItems,
+                event_details=event_details,
+            ).generateAttendeeFreeBusyResponse())
+        except Exception as e:
</ins><span class="cx">             log.failure(
</span><span class="cx">                 &quot;Could not determine free busy information for recipient {cuaddr}&quot;,
</span><span class="cx">                 cuaddr=recipient.cuaddr, level=LogLevel.debug
</span><span class="cx">             )
</span><span class="cx">             log.error(
</span><del>-                &quot;Could not determine free busy information for recipient {cuaddr}&quot;,
-                cuaddr=recipient.cuaddr
</del><ins>+                &quot;Could not determine free busy information for recipient {cuaddr}: {ex}&quot;,
+                cuaddr=recipient.cuaddr, ex=e
</ins><span class="cx">             )
</span><span class="cx">             err = HTTPError(ErrorResponse(
</span><span class="cx">                 responsecode.FORBIDDEN,
</span><span class="lines">@@ -245,56 +233,3 @@
</span><span class="cx">                 calendar=fbresult
</span><span class="cx">             )
</span><span class="cx">             returnValue(True)
</span><del>-
-
-    @inlineCallbacks
-    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):
-
-        # Find the current recipients calendars that are not transparent
-        fbset = (yield recipient.inbox.ownerHome().loadCalendars())
-        fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]
-
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-
-        # Process the availability property from the Inbox.
-        availability = recipient.inbox.ownerHome().getAvailability()
-        if availability is not None:
-            processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
-
-        # Check to see if the recipient is the same calendar user as the organizer.
-        # Needed for masked UID stuff.
-        if isinstance(self.scheduler.organizer, LocalCalendarUser):
-            same_calendar_user = self.scheduler.organizer.record.uid == recipient.record.uid
-        else:
-            same_calendar_user = False
-
-        # Now process free-busy set calendars
-        matchtotal = 0
-        for calendar in fbset:
-            matchtotal = (yield generateFreeBusyInfo(
-                calendar,
-                fbinfo,
-                self.scheduler.timeRange,
-                matchtotal,
-                excludeuid=self.scheduler.excludeUID,
-                organizer=self.scheduler.organizer.cuaddr,
-                organizerPrincipal=organizerPrincipal,
-                same_calendar_user=same_calendar_user,
-                servertoserver=remote,
-                event_details=event_details,
-                logItems=self.scheduler.logItems,
-            ))
-
-        # Build VFREEBUSY iTIP reply for this recipient
-        fbresult = buildFreeBusyResult(
-            fbinfo,
-            self.scheduler.timeRange,
-            organizer=organizerProp,
-            attendee=attendeeProp,
-            uid=uid,
-            method=&quot;REPLY&quot;,
-            event_details=event_details,
-        )
-
-        returnValue(fbresult)
</del></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingfreebusypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -35,28 +35,26 @@
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><span class="cx"> from txdav.caldav.icalendarstore import QueryMaxResources
</span><ins>+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
</ins><span class="cx"> from txdav.common.icommondatastore import IndexedSearchException, \
</span><span class="cx">     InternalDataStoreError
</span><span class="cx"> 
</span><span class="cx"> import uuid
</span><ins>+from collections import namedtuple
</ins><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="cx"> 
</span><del>-fbtype_mapper = {&quot;BUSY&quot;: 0, &quot;BUSY-TENTATIVE&quot;: 1, &quot;BUSY-UNAVAILABLE&quot;: 2}
-fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
</del><span class="cx"> 
</span><del>-fbcacher = Memcacher(&quot;FBCache&quot;, pickle=True)
-
-
-
</del><span class="cx"> class FBCacheEntry(object):
</span><span class="cx"> 
</span><span class="cx">     CACHE_DAYS_FLOATING_ADJUST = 1
</span><span class="cx"> 
</span><ins>+    fbcacher = Memcacher(&quot;FBCache&quot;, pickle=True)
+
</ins><span class="cx">     def __init__(self, key, token, timerange, fbresults):
</span><span class="cx">         self.key = key
</span><span class="cx">         self.token = token
</span><del>-        self.timerange = timerange
</del><ins>+        self.timerange = timerange.getText()
</ins><span class="cx">         self.fbresults = fbresults
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -66,16 +64,17 @@
</span><span class="cx"> 
</span><span class="cx">         key = str(calresource.id()) + &quot;/&quot; + useruid
</span><span class="cx">         token = (yield calresource.syncToken())
</span><del>-        entry = (yield fbcacher.get(key))
</del><ins>+        entry = (yield cls.fbcacher.get(key))
</ins><span class="cx"> 
</span><span class="cx">         if entry:
</span><span class="cx"> 
</span><span class="cx">             # Offset one day at either end to account for floating
</span><del>-            cached_start = entry.timerange.start + Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
-            cached_end = entry.timerange.end - Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
</del><ins>+            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)
</ins><span class="cx"> 
</span><span class="cx">             # Verify that the requested time range lies within the cache time range
</span><del>-            if compareDateTime(timerange.end, cached_end) &lt;= 0 and compareDateTime(timerange.start, cached_start) &gt;= 0:
</del><ins>+            if compareDateTime(timerange.getEnd(), cached_end) &lt;= 0 and compareDateTime(timerange.getStart(), cached_start) &gt;= 0:
</ins><span class="cx"> 
</span><span class="cx">                 # Verify that cached entry is still valid
</span><span class="cx">                 if token == entry.token:
</span><span class="lines">@@ -91,693 +90,700 @@
</span><span class="cx">         key = str(calresource.id()) + &quot;/&quot; + useruid
</span><span class="cx">         token = (yield calresource.syncToken())
</span><span class="cx">         entry = cls(key, token, timerange, fbresults)
</span><del>-        yield fbcacher.set(key, entry)
</del><ins>+        yield cls.fbcacher.set(key, entry)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-def generateFreeBusyInfo(
-    calresource,
-    fbinfo,
-    timerange,
-    matchtotal,
-    excludeuid=None,
-    organizer=None,
-    organizerPrincipal=None,
-    same_calendar_user=False,
-    servertoserver=False,
-    event_details=None,
-    logItems=None,
-    accountingItems=None,
-):
</del><ins>+class FreebusyQuery(object):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    Get freebusy information for a calendar. Different behavior for internal vs external calendars.
-
-    See L{_internalGenerateFreeBusyInfo} for argument description.
</del><ins>+    Class that manages the process of getting free busy information of a particular attendee.
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-    # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
-    # can be split across L{CalendarObject} and L{CalendarObjectExternal}
-    if calresource.external():
-        return _externalGenerateFreeBusyInfo(
-            calresource,
-            fbinfo,
-            timerange,
-            matchtotal,
-            excludeuid,
-            organizer,
-            organizerPrincipal,
-            same_calendar_user,
-            servertoserver,
-            event_details,
-            logItems,
-            accountingItems,
-        )
-    else:
-        return _internalGenerateFreeBusyInfo(
-            calresource,
-            fbinfo,
-            timerange,
-            matchtotal,
-            excludeuid,
-            organizer,
-            organizerPrincipal,
-            same_calendar_user,
-            servertoserver,
-            event_details,
-            logItems,
-            accountingItems,
-        )
</del><ins>+    FBInfo = namedtuple(&quot;FBInfo&quot;, (&quot;busy&quot;, &quot;tentative&quot;, &quot;unavailable&quot;,))
+    FBInfo_mapper = {&quot;BUSY&quot;: &quot;busy&quot;, &quot;BUSY-TENTATIVE&quot;: &quot;tentative&quot;, &quot;BUSY-UNAVAILABLE&quot;: &quot;unavailable&quot;}
+    FBInfo_index_mapper = {'B': &quot;busy&quot;, 'T': &quot;tentative&quot;, 'U': &quot;unavailable&quot;}
</ins><span class="cx"> 
</span><ins>+    def __init__(self, organizer, organizerProp, recipient, attendeeProp, uid, timeRange, excludeUID, logItems, accountingItems=None, event_details=None):
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><ins>+        @param organizer: the organizer making the freebusy request
+        @type organizer: L{CalendarUser}
+        @param organizerProp: iCalendar ORGANIZER property from the request
+        @type organizerProp: L{Property}
+        @param recipient: the attendee whose freebusy is being requested
+        @type recipient: L{CalendarUser}
+        @param attendeeProp: iCalendar ATTENDEE property from the request
+        @type attendeeProp: L{Property}
+        @param uid: iCalendar UID in the request
+        @type uid: L{str}
+        @param timeRange: time range for freebusy request
+        @type timeRange: L{Period}
+        @param excludeUID: an iCalendar UID to exclude from busy results
+        @type excludeUID: L{str}
+        @param logItems: items to add to logging
+        @type logItems: L{dict}
+        @param accountingItems: items to add to accounting logging
+        @type accountingItems: L{dict}
+        @param event_details: if not L{None}, a list in which busy event details are stored
+        @type event_details: L{list} or L{None}
+        &quot;&quot;&quot;
+        self.organizer = organizer
+        self.organizerProp = organizerProp
+        self.recipient = recipient
+        self.attendeeProp = attendeeProp
+        self.uid = uid
+        self.timerange = timeRange
+        self.excludeuid = excludeUID
+        self.logItems = logItems
+        self.accountingItems = accountingItems
+        self.event_details = event_details
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-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,
-):
-    &quot;&quot;&quot;
-    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.
</del><ins>+        # Check to see if the recipient is the same calendar user as the organizer.
+        # Needed for masked UID stuff.
+        if isinstance(self.organizer, LocalCalendarUser):
+            self.same_calendar_user = self.organizer.record.uid == self.recipient.record.uid
+        else:
+            self.same_calendar_user = False
</ins><span class="cx"> 
</span><del>-    See L{_internalGenerateFreeBusyInfo} for argument description.
-    &quot;&quot;&quot;
-    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)
</del><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def generateAttendeeFreeBusyResponse(self, fbset=None, method=&quot;REPLY&quot;):
</ins><span class="cx"> 
</span><ins>+        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+        fbinfo = self.FBInfo([], [], [])
</ins><span class="cx"> 
</span><del>-@inlineCallbacks
-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,
-):
-    &quot;&quot;&quot;
-    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
-    &quot;&quot;&quot;
</del><ins>+        if self.recipient is not None:
+            # Find the current recipients calendars that are not transparent
+            if fbset is None:
+                fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
+                fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]
</ins><span class="cx"> 
</span><del>-    # 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
</del><ins>+            # Process the availability property from the Inbox.
+            availability = self.recipient.inbox.ownerHome().getAvailability()
+            if availability is not None:
+                self.processAvailabilityFreeBusy(availability, fbinfo)
</ins><span class="cx"> 
</span><del>-    # May need organizer principal
-    organizer_record = (yield calresource.directoryService().recordWithCalendarUserAddress(organizer)) if organizer else None
-    organizer_uid = organizer_record.uid if organizer_record else &quot;&quot;
</del><ins>+        # Now process free-busy set calendars
+        matchtotal = 0
+        for calendar in fbset:
+            matchtotal = (yield self.generateFreeBusyInfo(
+                calendar,
+                fbinfo,
+                matchtotal,
+            ))
</ins><span class="cx"> 
</span><del>-    # Free busy is per-user
-    attendee_uid = calresource.viewerHome().uid()
-    attendee_record = yield calresource.directoryService().recordWithUID(attendee_uid.decode(&quot;utf-8&quot;))
</del><ins>+        # Build VFREEBUSY iTIP reply for this recipient
+        fbresult = self.buildFreeBusyResult(fbinfo, method=method)
</ins><span class="cx"> 
</span><del>-    # Get the timezone property from the collection.
-    tz = calresource.getTimezone()
</del><ins>+        returnValue(fbresult)
</ins><span class="cx"> 
</span><del>-    # Look for possible extended free busy information
-    rich_options = {
-        &quot;organizer&quot;: False,
-        &quot;delegate&quot;: False,
-        &quot;resource&quot;: False,
-    }
-    do_event_details = False
-    if event_details is not None and organizer_record is not None and attendee_record is not None:
</del><span class="cx"> 
</span><del>-        # 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(&quot;utf-8&quot;))
</del><ins>+    def processAvailabilityFreeBusy(self, calendar, fbinfo):
+        &quot;&quot;&quot;
+        Extract free-busy data from a VAVAILABILITY component.
</ins><span class="cx"> 
</span><del>-        # 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[&quot;organizer&quot;] = True
</del><ins>+        @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.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-        # 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[&quot;delegate&quot;] = True
</del><ins>+        for vav in [x for x in calendar.subcomponents() if x.name() == &quot;VAVAILABILITY&quot;]:
</ins><span class="cx"> 
</span><del>-        # Check if attendee is room or resource
-        if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in (&quot;RESOURCE&quot;, &quot;ROOM&quot;,):
-            do_event_details = True
-            rich_options[&quot;resource&quot;] = True
</del><ins>+            # 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, self.timerange)
+            if overall is None:
+                continue
</ins><span class="cx"> 
</span><del>-    # Try cache
-    resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, timerange)) if config.EnableFreeBusyCache else None
</del><ins>+            # Now get periods for each instance of AVAILABLE sub-components
+            periods = self.processAvailablePeriods(vav)
</ins><span class="cx"> 
</span><del>-    if resources is None:
</del><ins>+            # Now invert the periods and store in accumulator
+            busyperiods = []
+            last_end = self.timerange.getStart()
+            for period in periods:
+                if last_end &lt; period.getStart():
+                    busyperiods.append(Period(last_end, period.getStart()))
+                last_end = period.getEnd()
+            if last_end &lt; self.timerange.getEnd():
+                busyperiods.append(Period(last_end, self.timerange.getEnd()))
</ins><span class="cx"> 
</span><del>-        if accountingItems is not None:
-            accountingItems[&quot;fb-uncached&quot;] = accountingItems.get(&quot;fb-uncached&quot;, 0) + 1
</del><ins>+            # Add to actual results mapped by busy type
+            fbtype = vav.propertyValue(&quot;BUSYTYPE&quot;)
+            if fbtype is None:
+                fbtype = &quot;BUSY-UNAVAILABLE&quot;
</ins><span class="cx"> 
</span><del>-        caching = False
-        if config.EnableFreeBusyCache:
-            # Log extended item
-            if logItems is not None:
-                logItems[&quot;fb-uncached&quot;] = logItems.get(&quot;fb-uncached&quot;, 0) + 1
</del><ins>+            getattr(fbinfo, self.FBInfo_mapper.get(fbtype, &quot;unavailable&quot;)).extend(busyperiods)
</ins><span class="cx"> 
</span><del>-            # 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))
</del><span class="cx"> 
</span><del>-            # If the requested time range would fit in our allowed cache range, trigger the cache creation
-            if compareDateTime(timerange.start, cache_start) &gt;= 0 and compareDateTime(timerange.end, cache_end) &lt;= 0:
-                cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText())
-                caching = True
</del><ins>+    def processAvailablePeriods(self, calendar):
+        &quot;&quot;&quot;
+        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.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-        #
-        # 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.
-        #
</del><ins>+        periods = []
</ins><span class="cx"> 
</span><del>-        # Create fake filter element to match time-range
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                caldavxml.ComponentFilter(
-                    cache_timerange if caching else timerange,
-                    name=(&quot;VEVENT&quot;, &quot;VFREEBUSY&quot;, &quot;VAVAILABILITY&quot;),
-                ),
-                name=&quot;VCALENDAR&quot;,
-            )
-        )
-        filter = Filter(filter)
-        tzinfo = filter.settimezone(tz)
-        if accountingItems is not None:
-            tr = cache_timerange if caching else timerange
-            accountingItems[&quot;fb-query-timerange&quot;] = (str(tr.start), str(tr.end),)
</del><ins>+        # First we need to group all AVAILABLE sub-components by UID
+        uidmap = {}
+        for component in calendar.subcomponents():
+            if component.name() == &quot;AVAILABLE&quot;:
+                uid = component.propertyValue(&quot;UID&quot;)
+                uidmap.setdefault(uid, []).append(component)
</ins><span class="cx"> 
</span><del>-        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(&quot;Invalid indexedSearch query&quot;)
</del><ins>+        # Then we expand each uid set separately
+        for componentSet in uidmap.itervalues():
+            instances = InstanceList(ignoreInvalidInstances=True)
+            instances.expandTimeRanges(componentSet, self.timerange.getEnd())
</ins><span class="cx"> 
</span><del>-    else:
-        if accountingItems is not None:
-            accountingItems[&quot;fb-cached&quot;] = accountingItems.get(&quot;fb-cached&quot;, 0) + 1
</del><ins>+            # 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
</ins><span class="cx"> 
</span><del>-        # Log extended item
-        if logItems is not None:
-            logItems[&quot;fb-cached&quot;] = logItems.get(&quot;fb-cached&quot;, 0) + 1
</del><ins>+                # Clip period for this instance - use duration for period end if that
+                # is what original component used
+                if instance.component.hasProperty(&quot;DURATION&quot;):
+                    period = Period(start, duration=end - start)
+                else:
+                    period = Period(start, end)
+                clipped = clipPeriod(period, self.timerange)
+                if clipped:
+                    periods.append(clipped)
</ins><span class="cx"> 
</span><del>-        # Determine appropriate timezone (UTC is the default)
-        tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
</del><ins>+        normalizePeriodList(periods)
+        return periods
</ins><span class="cx"> 
</span><del>-    # 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,))
</del><span class="cx"> 
</span><del>-    if accountingItems is not None:
-        accountingItems[&quot;fb-resources&quot;] = {}
-        for k, v in aggregated_resources.items():
-            name, uid, type, test_organizer = k
-            accountingItems[&quot;fb-resources&quot;][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[&quot;fb-resources&quot;][uid].append((
-                    float,
-                    str(fbstart),
-                    str(fbend),
-                    fbtype,
-                ))
</del><ins>+    def generateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+        &quot;&quot;&quot;
+        Get freebusy information for a calendar. Different behavior for internal vs external calendars.
</ins><span class="cx"> 
</span><del>-    # 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():
</del><ins>+        See L{_internalGenerateFreeBusyInfo} for argument description.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-        name, uid, type, test_organizer = key
</del><ins>+        # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
+        # can be split across L{CalendarObject} and L{CalendarObjectExternal}
+        if calresource.external():
+            return self._externalGenerateFreeBusyInfo(
+                calresource,
+                fbinfo,
+                matchtotal,
+            )
+        else:
+            return self._internalGenerateFreeBusyInfo(
+                calresource,
+                fbinfo,
+                matchtotal,
+            )
</ins><span class="cx"> 
</span><del>-        # Short-cut - if an fbtype exists we can use that
-        if type == &quot;VEVENT&quot; and aggregated_resources[key][0][3] != '?':
</del><span class="cx"> 
</span><del>-            matchedResource = False
</del><ins>+    @inlineCallbacks
+    def _externalGenerateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+        &quot;&quot;&quot;
+        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.
</ins><span class="cx"> 
</span><del>-            # Look at each instance
-            for float, start, end, fbtype in aggregated_resources[key]:
-                # Ignore free time or unknown
-                if fbtype in ('F', '?'):
-                    continue
</del><ins>+        See L{_internalGenerateFreeBusyInfo} for argument description.
+        &quot;&quot;&quot;
+        fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(
+            calresource, self.organizer.cuaddr, self.recipient.cuaddr, 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)
</ins><span class="cx"> 
</span><del>-                # 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 &quot;&quot;
-                                recordUIDCache[test_organizer] = test_uid
-                        else:
-                            test_uid = &quot;&quot;
</del><span class="cx"> 
</span><del>-                        # 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 == &quot;&quot;) and same_calendar_user:
-                            continue
</del><ins>+    @inlineCallbacks
+    def _internalGenerateFreeBusyInfo(
+        self,
+        calresource,
+        fbinfo,
+        matchtotal,
+    ):
+        &quot;&quot;&quot;
+        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.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-                # 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))
</del><ins>+        # 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
</ins><span class="cx"> 
</span><del>-                # Clip instance to time range
-                clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end))
</del><ins>+        # May need organizer principal
+        organizer_record = self.organizer.record if self.organizer and self.organizer.hosted() else None
+        organizer_uid = organizer_record.uid if organizer_record else &quot;&quot;
</ins><span class="cx"> 
</span><del>-                # Double check for overlap
-                if clipped:
-                    matchedResource = True
-                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)
</del><ins>+        # Free busy is per-user
+        attendee_record = self.recipient.record if self.organizer and self.recipient.hosted() else None
+        attendee_uid = attendee_record.uid if attendee_record else calresource.viewerHome().uid()
</ins><span class="cx"> 
</span><del>-            if matchedResource:
-                # Check size of results is within limit
-                matchtotal += 1
-                if matchtotal &gt; config.MaxQueryWithDataResults:
-                    raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</del><ins>+        # Get the timezone property from the collection.
+        tz = calresource.getTimezone()
</ins><span class="cx"> 
</span><del>-                # 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)
</del><ins>+        # Look for possible extended free busy information
+        rich_options = {
+            &quot;organizer&quot;: False,
+            &quot;delegate&quot;: False,
+            &quot;resource&quot;: False,
+        }
+        do_event_details = False
+        if self.event_details is not None and organizer_record is not None and attendee_record is not None:
</ins><span class="cx"> 
</span><del>-        else:
-            child = (yield calresource.calendarObjectWithName(name))
-            calendar = (yield child.componentForUser())
</del><ins>+            # 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(&quot;utf-8&quot;))
</ins><span class="cx"> 
</span><del>-            # 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(&quot;Calendar %s is missing from calendar collection %r&quot; % (name, calresource))
-                continue
</del><ins>+            # 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[&quot;organizer&quot;] = True
</ins><span class="cx"> 
</span><del>-            # 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 &quot;&quot;
-                            recordUIDCache[test_organizer] = test_uid
-                    else:
-                        test_uid = &quot;&quot;
</del><ins>+            # 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[&quot;delegate&quot;] = True
</ins><span class="cx"> 
</span><del>-                    # 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
</del><ins>+            # Check if attendee is room or resource
+            if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in (&quot;RESOURCE&quot;, &quot;ROOM&quot;,):
+                do_event_details = True
+                rich_options[&quot;resource&quot;] = True
</ins><span class="cx"> 
</span><del>-            if accountingItems is not None:
-                accountingItems.setdefault(&quot;fb-filter-match&quot;, []).append(uid)
</del><ins>+        # Try cache
+        resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None
</ins><span class="cx"> 
</span><del>-            if filter.match(calendar, None):
-                if accountingItems is not None:
-                    accountingItems.setdefault(&quot;fb-filter-matched&quot;, []).append(uid)
</del><ins>+        if resources is None:
</ins><span class="cx"> 
</span><del>-                # Check size of results is within limit
-                matchtotal += 1
-                if matchtotal &gt; config.MaxQueryWithDataResults:
-                    raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</del><ins>+            if self.accountingItems is not None:
+                self.accountingItems[&quot;fb-uncached&quot;] = self.accountingItems.get(&quot;fb-uncached&quot;, 0) + 1
</ins><span class="cx"> 
</span><del>-                if calendar.mainType() == &quot;VEVENT&quot;:
-                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
-                elif calendar.mainType() == &quot;VFREEBUSY&quot;:
-                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
-                elif calendar.mainType() == &quot;VAVAILABILITY&quot;:
-                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
-                else:
-                    assert &quot;Free-busy query returned unwanted component: %s in %r&quot;, (name, calresource,)
</del><ins>+            caching = False
+            if config.EnableFreeBusyCache:
+                # Log extended item
+                if self.logItems is not None:
+                    self.logItems[&quot;fb-uncached&quot;] = self.logItems.get(&quot;fb-uncached&quot;, 0) + 1
</ins><span class="cx"> 
</span><del>-                # Add extended details
-                if calendar.mainType() == &quot;VEVENT&quot; 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)
</del><ins>+                # 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))
</ins><span class="cx"> 
</span><del>-    returnValue(matchtotal)
</del><ins>+                # If the requested time range would fit in our allowed cache range, trigger the cache creation
+                if compareDateTime(self.timerange.getStart(), cache_start) &gt;= 0 and compareDateTime(self.timerange.getEnd(), cache_end) &lt;= 0:
+                    cache_timerange = Period(cache_start, cache_end)
+                    caching = True
</ins><span class="cx"> 
</span><ins>+            #
+            # 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.
+            #
</ins><span class="cx"> 
</span><ins>+            # 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=(&quot;VEVENT&quot;, &quot;VFREEBUSY&quot;, &quot;VAVAILABILITY&quot;),
+                    ),
+                    name=&quot;VCALENDAR&quot;,
+                )
+            )
+            filter = Filter(filter)
+            tzinfo = filter.settimezone(tz)
+            if self.accountingItems is not None:
+                self.accountingItems[&quot;fb-query-timerange&quot;] = (str(tr.start), str(tr.end),)
</ins><span class="cx"> 
</span><del>-def _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo):
-    &quot;&quot;&quot;
-    Expand events within the specified time range and limit the set of properties to those allowed for
-    delegate extended free busy.
</del><ins>+            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(&quot;Invalid indexedSearch query&quot;)
</ins><span class="cx"> 
</span><del>-    @param calendar: the calendar object to expand
-    @type calendar: L{Component}
-    @param event_details: list to append VEVENT components to
-    @type event_details: C{list}
-    @param timerange: the time-range in which to expand
-    @type timerange: L{TimeRange}
-    @param tzinfo: timezone for floating time calculations
-    @type tzinfo: L{Timezone}
-    &quot;&quot;&quot;
</del><ins>+        else:
+            if self.accountingItems is not None:
+                self.accountingItems[&quot;fb-cached&quot;] = self.accountingItems.get(&quot;fb-cached&quot;, 0) + 1
</ins><span class="cx"> 
</span><del>-    # First expand the component
-    expanded = calendar.expand(timerange.start, timerange.end, timezone=tzinfo)
</del><ins>+            # Log extended item
+            if self.logItems is not None:
+                self.logItems[&quot;fb-cached&quot;] = self.logItems.get(&quot;fb-cached&quot;, 0) + 1
</ins><span class="cx"> 
</span><del>-    keep_props = (
-        &quot;UID&quot;,
-        &quot;RECURRENCE-ID&quot;,
-        &quot;DTSTAMP&quot;,
-        &quot;DTSTART&quot;,
-        &quot;DTEND&quot;,
-        &quot;DURATION&quot;,
-    )
</del><ins>+            # Determine appropriate timezone (UTC is the default)
+            tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
</ins><span class="cx"> 
</span><del>-    if rich_options[&quot;organizer&quot;] or rich_options[&quot;delegate&quot;]:
-        keep_props += (&quot;SUMMARY&quot;,)
</del><ins>+        # 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,))
</ins><span class="cx"> 
</span><del>-    if rich_options[&quot;organizer&quot;] or rich_options[&quot;resource&quot;]:
-        keep_props += (&quot;ORGANIZER&quot;,)
</del><ins>+        if self.accountingItems is not None:
+            self.accountingItems[&quot;fb-resources&quot;] = {}
+            for k, v in aggregated_resources.items():
+                name, uid, type, test_organizer = k
+                self.accountingItems[&quot;fb-resources&quot;][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))
+                    self.accountingItems[&quot;fb-resources&quot;][uid].append((
+                        float,
+                        str(fbstart),
+                        str(fbend),
+                        fbtype,
+                    ))
</ins><span class="cx"> 
</span><del>-    # Remove all but essential properties
-    expanded.filterProperties(keep=keep_props)
</del><ins>+        # 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():
</ins><span class="cx"> 
</span><del>-    # Need to remove all child components of VEVENT
-    for subcomponent in expanded.subcomponents():
-        if subcomponent.name() == &quot;VEVENT&quot;:
-            for sub in tuple(subcomponent.subcomponents()):
-                subcomponent.removeComponent(sub)
</del><ins>+            name, uid, type, test_organizer = key
</ins><span class="cx"> 
</span><del>-    event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == &quot;VEVENT&quot;])
</del><ins>+            # Short-cut - if an fbtype exists we can use that
+            if type == &quot;VEVENT&quot; and aggregated_resources[key][0][3] != '?':
</ins><span class="cx"> 
</span><ins>+                matchedResource = False
</ins><span class="cx"> 
</span><ins>+                # Look at each instance
+                for float, start, end, fbtype in aggregated_resources[key]:
+                    # Ignore free time or unknown
+                    if fbtype in ('F', '?'):
+                        continue
</ins><span class="cx"> 
</span><del>-def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
-    &quot;&quot;&quot;
-    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 timerange: the time range to restrict free busy data to.
-    @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
-    &quot;&quot;&quot;
</del><ins>+                    # Ignore ones of this UID
+                    if self.excludeuid:
+                        # See if we have a UID match
+                        if (self.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 &quot;&quot;
+                                    recordUIDCache[test_organizer] = test_uid
+                            else:
+                                test_uid = &quot;&quot;
</ins><span class="cx"> 
</span><del>-    # Expand out the set of instances for the event with in the required range
-    instances = calendar.expandTimeRanges(timerange.end, lowerLimit=timerange.start, ignoreInvalidInstances=True)
</del><ins>+                            # Check that ORGANIZER's match (security requirement)
+                            if (self.organizer is None) or (organizer_uid == test_uid):
+                                continue
+                            # Check for no ORGANIZER and check by same calendar user
+                            elif (test_uid == &quot;&quot;) and self.same_calendar_user:
+                                continue
</ins><span class="cx"> 
</span><del>-    # Can only do timed events
-    for key in instances:
-        instance = instances[key]
-        if instance.start.isDateOnly():
-            return
-        break
-    else:
-        return
</del><ins>+                    # 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))
</ins><span class="cx"> 
</span><del>-    for key in instances:
-        instance = instances[key]
</del><ins>+                    # Clip instance to time range
+                    clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), self.timerange)
</ins><span class="cx"> 
</span><del>-        # 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)
</del><ins>+                    # Double check for overlap
+                    if clipped:
+                        matchedResource = True
+                        getattr(fbinfo, self.FBInfo_index_mapper.get(fbtype, &quot;busy&quot;)).append(clipped)
</ins><span class="cx"> 
</span><del>-        # Check TRANSP property of underlying component
-        if instance.component.hasProperty(&quot;TRANSP&quot;):
-            # If its TRANSPARENT we always ignore it
-            if instance.component.propertyValue(&quot;TRANSP&quot;) == &quot;TRANSPARENT&quot;:
-                continue
</del><ins>+                if matchedResource:
+                    # Check size of results is within limit
+                    matchtotal += 1
+                    if matchtotal &gt; config.MaxQueryWithDataResults:
+                        raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</ins><span class="cx"> 
</span><del>-        # Determine status
-        if instance.component.hasProperty(&quot;STATUS&quot;):
-            status = instance.component.propertyValue(&quot;STATUS&quot;)
-        else:
-            status = &quot;CONFIRMED&quot;
</del><ins>+                    # 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())
+                            self._addEventDetails(calendar, rich_options, tzinfo)
</ins><span class="cx"> 
</span><del>-        # Ignore cancelled
-        if status == &quot;CANCELLED&quot;:
-            continue
</del><ins>+            else:
+                child = (yield calresource.calendarObjectWithName(name))
+                calendar = (yield child.componentForUser())
</ins><span class="cx"> 
</span><del>-        # Clip period for this instance - use duration for period end if that
-        # is what original component used
-        if instance.component.hasProperty(&quot;DURATION&quot;):
-            period = Period(fbstart, duration=fbend - fbstart)
-        else:
-            period = Period(fbstart, fbend)
-        clipped = clipPeriod(period, Period(timerange.start, timerange.end))
</del><ins>+                # 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(&quot;Calendar %s is missing from calendar collection %r&quot; % (name, calresource))
+                    continue
</ins><span class="cx"> 
</span><del>-        # Double check for overlap
-        if clipped:
-            if status == &quot;TENTATIVE&quot;:
-                fbinfo[1].append(clipped)
-            else:
-                fbinfo[0].append(clipped)
</del><ins>+                # Ignore ones of this UID
+                if self.excludeuid:
+                    # See if we have a UID match
+                    if (self.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 &quot;&quot;
+                                recordUIDCache[test_organizer] = test_uid
+                        else:
+                            test_uid = &quot;&quot;
</ins><span class="cx"> 
</span><ins>+                        # Check that ORGANIZER's match (security requirement)
+                        if (self.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 self.same_calendar_user:
+                            continue
</ins><span class="cx"> 
</span><ins>+                if self.accountingItems is not None:
+                    self.accountingItems.setdefault(&quot;fb-filter-match&quot;, []).append(uid)
</ins><span class="cx"> 
</span><del>-def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
-    &quot;&quot;&quot;
-    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.
-    &quot;&quot;&quot;
</del><ins>+                if filter.match(calendar, None):
+                    if self.accountingItems is not None:
+                        self.accountingItems.setdefault(&quot;fb-filter-matched&quot;, []).append(uid)
</ins><span class="cx"> 
</span><del>-    for vfb in [x for x in calendar.subcomponents() if x.name() == &quot;VFREEBUSY&quot;]:
-        # 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
</del><ins>+                    # Check size of results is within limit
+                    matchtotal += 1
+                    if matchtotal &gt; config.MaxQueryWithDataResults:
+                        raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</ins><span class="cx"> 
</span><del>-        # Now look at each FREEBUSY property
-        for fb in vfb.properties(&quot;FREEBUSY&quot;):
-            # Check the type
-            fbtype = fb.parameterValue(&quot;FBTYPE&quot;, default=&quot;BUSY&quot;)
-            if fbtype == &quot;FREE&quot;:
-                continue
</del><ins>+                    if calendar.mainType() == &quot;VEVENT&quot;:
+                        self.processEventFreeBusy(calendar, fbinfo, tzinfo)
+                    elif calendar.mainType() == &quot;VFREEBUSY&quot;:
+                        self.processFreeBusyFreeBusy(calendar, fbinfo)
+                    elif calendar.mainType() == &quot;VAVAILABILITY&quot;:
+                        self.processAvailabilityFreeBusy(calendar, fbinfo)
+                    else:
+                        assert &quot;Free-busy query returned unwanted component: %s in %r&quot;, (name, calresource,)
</ins><span class="cx"> 
</span><del>-            # Look at each period in the property
-            assert isinstance(fb.value(), list), &quot;FREEBUSY property does not contain a list of values: %r&quot; % (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)
</del><ins>+                    # Add extended details
+                    if calendar.mainType() == &quot;VEVENT&quot; 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())
+                            self._addEventDetails(calendar, rich_options, tzinfo)
</ins><span class="cx"> 
</span><ins>+        returnValue(matchtotal)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
-    &quot;&quot;&quot;
-    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.
-    &quot;&quot;&quot;
</del><ins>+    def _addEventDetails(self, calendar, rich_options, tzinfo):
+        &quot;&quot;&quot;
+        Expand events within the specified time range and limit the set of properties to those allowed for
+        delegate extended free busy.
</ins><span class="cx"> 
</span><del>-    for vav in [x for x in calendar.subcomponents() if x.name() == &quot;VAVAILABILITY&quot;]:
</del><ins>+        @param calendar: the calendar object to expand
+        @type calendar: L{Component}
+        @param event_details: list to append VEVENT components to
+        @type event_details: C{list}
+        @param tzinfo: timezone for floating time calculations
+        @type tzinfo: L{Timezone}
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-        # 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
</del><ins>+        # First expand the component
+        expanded = calendar.expand(self.timerange.getStart(), self.timerange.getEnd(), timezone=tzinfo)
</ins><span class="cx"> 
</span><del>-        # Now get periods for each instance of AVAILABLE sub-components
-        periods = processAvailablePeriods(vav, timerange)
</del><ins>+        keep_props = (
+            &quot;UID&quot;,
+            &quot;RECURRENCE-ID&quot;,
+            &quot;DTSTAMP&quot;,
+            &quot;DTSTART&quot;,
+            &quot;DTEND&quot;,
+            &quot;DURATION&quot;,
+        )
</ins><span class="cx"> 
</span><del>-        # Now invert the periods and store in accumulator
-        busyperiods = []
-        last_end = timerange.start
-        for period in periods:
-            if last_end &lt; period.getStart():
-                busyperiods.append(Period(last_end, period.getStart()))
-            last_end = period.getEnd()
-        if last_end &lt; timerange.end:
-            busyperiods.append(Period(last_end, timerange.end))
</del><ins>+        if rich_options[&quot;organizer&quot;] or rich_options[&quot;delegate&quot;]:
+            keep_props += (&quot;SUMMARY&quot;,)
</ins><span class="cx"> 
</span><del>-        # Add to actual results mapped by busy type
-        fbtype = vav.propertyValue(&quot;BUSYTYPE&quot;)
-        if fbtype is None:
-            fbtype = &quot;BUSY-UNAVAILABLE&quot;
</del><ins>+        if rich_options[&quot;organizer&quot;] or rich_options[&quot;resource&quot;]:
+            keep_props += (&quot;ORGANIZER&quot;,)
</ins><span class="cx"> 
</span><del>-        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
</del><ins>+        # Remove all but essential properties
+        expanded.filterProperties(keep=keep_props)
</ins><span class="cx"> 
</span><ins>+        # Need to remove all child components of VEVENT
+        for subcomponent in expanded.subcomponents():
+            if subcomponent.name() == &quot;VEVENT&quot;:
+                for sub in tuple(subcomponent.subcomponents()):
+                    subcomponent.removeComponent(sub)
</ins><span class="cx"> 
</span><ins>+        self.event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == &quot;VEVENT&quot;])
</ins><span class="cx"> 
</span><del>-def processAvailablePeriods(calendar, timerange):
-    &quot;&quot;&quot;
-    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.
-    &quot;&quot;&quot;
</del><span class="cx"> 
</span><del>-    periods = []
</del><ins>+    def processEventFreeBusy(self, calendar, fbinfo, tzinfo):
+        &quot;&quot;&quot;
+        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.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-    # First we need to group all AVAILABLE sub-components by UID
-    uidmap = {}
-    for component in calendar.subcomponents():
-        if component.name() == &quot;AVAILABLE&quot;:
-            uid = component.propertyValue(&quot;UID&quot;)
-            uidmap.setdefault(uid, []).append(component)
</del><ins>+        # 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)
</ins><span class="cx"> 
</span><del>-    # Then we expand each uid set separately
-    for componentSet in uidmap.itervalues():
-        instances = InstanceList(ignoreInvalidInstances=True)
-        instances.expandTimeRanges(componentSet, timerange.end)
</del><ins>+        # Can only do timed events
+        for key in instances:
+            instance = instances[key]
+            if instance.start.isDateOnly():
+                return
+            break
+        else:
+            return
</ins><span class="cx"> 
</span><del>-        # Now convert instances into period list
</del><span class="cx">         for key in instances:
</span><span class="cx">             instance = instances[key]
</span><del>-            # 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():
</del><ins>+
+            # 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(&quot;TRANSP&quot;):
+                # If its TRANSPARENT we always ignore it
+                if instance.component.propertyValue(&quot;TRANSP&quot;) == &quot;TRANSPARENT&quot;:
+                    continue
+
+            # Determine status
+            if instance.component.hasProperty(&quot;STATUS&quot;):
+                status = instance.component.propertyValue(&quot;STATUS&quot;)
+            else:
+                status = &quot;CONFIRMED&quot;
+
+            # Ignore cancelled
+            if status == &quot;CANCELLED&quot;:
</ins><span class="cx">                 continue
</span><del>-            end = instance.end
-            if end.floating():
-                continue
</del><span class="cx"> 
</span><span class="cx">             # Clip period for this instance - use duration for period end if that
</span><span class="cx">             # is what original component used
</span><span class="cx">             if instance.component.hasProperty(&quot;DURATION&quot;):
</span><del>-                period = Period(start, duration=end - start)
</del><ins>+                period = Period(fbstart, duration=fbend - fbstart)
</ins><span class="cx">             else:
</span><del>-                period = Period(start, end)
-            clipped = clipPeriod(period, Period(timerange.start, timerange.end))
</del><ins>+                period = Period(fbstart, fbend)
+            clipped = clipPeriod(period, self.timerange)
+
+            # Double check for overlap
</ins><span class="cx">             if clipped:
</span><del>-                periods.append(clipped)
</del><ins>+                if status == &quot;TENTATIVE&quot;:
+                    fbinfo.tentative.append(clipped)
+                else:
+                    fbinfo.busy.append(clipped)
</ins><span class="cx"> 
</span><del>-    normalizePeriodList(periods)
-    return periods
</del><span class="cx"> 
</span><ins>+    def processFreeBusyFreeBusy(self, calendar, fbinfo):
+        &quot;&quot;&quot;
+        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.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><ins>+        for vfb in [x for x in calendar.subcomponents() if x.name() == &quot;VFREEBUSY&quot;]:
+            # First check any start/end in the actual component
+            start = vfb.getStartDateUTC()
+            end = vfb.getEndDateUTC()
+            if start and end:
+                if not timeRangesOverlap(start, end, self.timerange.getStart(), self.timerange.getEnd()):
+                    continue
</ins><span class="cx"> 
</span><del>-def buildFreeBusyResult(fbinfo, timerange, organizer=None, attendee=None, uid=None, method=None, event_details=None):
-    &quot;&quot;&quot;
-    Generate a VCALENDAR object containing a single VFREEBUSY that is the
-    aggregate of the free busy info passed in.
</del><ins>+            # Now look at each FREEBUSY property
+            for fb in vfb.properties(&quot;FREEBUSY&quot;):
+                # Check the type
+                fbtype = fb.parameterValue(&quot;FBTYPE&quot;, default=&quot;BUSY&quot;)
+                if fbtype == &quot;FREE&quot;:
+                    continue
</ins><span class="cx"> 
</span><del>-    @param fbinfo:        the array of busy periods to use.
-    @param timerange:     the L{TimeRange} for the query.
-    @param organizer:     the L{Property} for the Organizer of the free busy request, or None.
-    @param attendee:      the L{Property} for the Attendee responding to the free busy request, or None.
-    @param uid:           the UID value from the free busy request.
-    @param method:        the METHOD property value to insert.
-    @param event_details: VEVENT components to add.
-    @return:              the L{Component} containing the calendar data.
-    &quot;&quot;&quot;
</del><ins>+                # Look at each period in the property
+                assert isinstance(fb.value(), list), &quot;FREEBUSY property does not contain a list of values: %r&quot; % (fb,)
+                for period in fb.value():
+                    # Clip period for this instance
+                    clipped = clipPeriod(period.getValue(), self.timerange)
+                    if clipped:
+                        getattr(fbinfo, self.FBInfo_mapper.get(fbtype, &quot;busy&quot;)).append(clipped)
</ins><span class="cx"> 
</span><del>-    # Merge overlapping time ranges in each fb info section
-    normalizePeriodList(fbinfo[0])
-    normalizePeriodList(fbinfo[1])
-    normalizePeriodList(fbinfo[2])
</del><span class="cx"> 
</span><del>-    # Now build a new calendar object with the free busy info we have
-    fbcalendar = Component(&quot;VCALENDAR&quot;)
-    fbcalendar.addProperty(Property(&quot;VERSION&quot;, &quot;2.0&quot;))
-    fbcalendar.addProperty(Property(&quot;PRODID&quot;, iCalendarProductID))
-    if method:
-        fbcalendar.addProperty(Property(&quot;METHOD&quot;, method))
-    fb = Component(&quot;VFREEBUSY&quot;)
-    fbcalendar.addComponent(fb)
-    if organizer is not None:
-        fb.addProperty(organizer)
-    if attendee is not None:
-        fb.addProperty(attendee)
-    fb.addProperty(Property(&quot;DTSTART&quot;, timerange.start))
-    fb.addProperty(Property(&quot;DTEND&quot;, timerange.end))
-    fb.addProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-    if len(fbinfo[0]) != 0:
-        fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo[0], {&quot;FBTYPE&quot;: &quot;BUSY&quot;}))
-    if len(fbinfo[1]) != 0:
-        fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo[1], {&quot;FBTYPE&quot;: &quot;BUSY-TENTATIVE&quot;}))
-    if len(fbinfo[2]) != 0:
-        fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo[2], {&quot;FBTYPE&quot;: &quot;BUSY-UNAVAILABLE&quot;}))
-    if uid is not None:
-        fb.addProperty(Property(&quot;UID&quot;, uid))
-    else:
-        uid = str(uuid.uuid4())
-        fb.addProperty(Property(&quot;UID&quot;, uid))
</del><ins>+    def buildFreeBusyResult(self, fbinfo, method=None):
+        &quot;&quot;&quot;
+        Generate a VCALENDAR object containing a single VFREEBUSY that is the
+        aggregate of the free busy info passed in.
</ins><span class="cx"> 
</span><del>-    if event_details:
-        for vevent in event_details:
-            fbcalendar.addComponent(vevent)
</del><ins>+        @param fbinfo:        the array of busy periods to use.
+        @param method:        the METHOD property value to insert.
+        @return:              the L{Component} containing the calendar data.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><del>-    return fbcalendar
</del><ins>+        # Merge overlapping time ranges in each fb info section
+        normalizePeriodList(fbinfo.busy)
+        normalizePeriodList(fbinfo.tentative)
+        normalizePeriodList(fbinfo.unavailable)
+
+        # Now build a new calendar object with the free busy info we have
+        fbcalendar = Component(&quot;VCALENDAR&quot;)
+        fbcalendar.addProperty(Property(&quot;VERSION&quot;, &quot;2.0&quot;))
+        fbcalendar.addProperty(Property(&quot;PRODID&quot;, iCalendarProductID))
+        if method:
+            fbcalendar.addProperty(Property(&quot;METHOD&quot;, method))
+        fb = Component(&quot;VFREEBUSY&quot;)
+        fbcalendar.addComponent(fb)
+        if self.organizerProp is not None:
+            fb.addProperty(self.organizerProp)
+        if self.attendeeProp is not None:
+            fb.addProperty(self.attendeeProp)
+        fb.addProperty(Property(&quot;DTSTART&quot;, self.timerange.getStart()))
+        fb.addProperty(Property(&quot;DTEND&quot;, self.timerange.getEnd()))
+        fb.addProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
+        if len(fbinfo.busy) != 0:
+            fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo.busy, {&quot;FBTYPE&quot;: &quot;BUSY&quot;}))
+        if len(fbinfo.tentative) != 0:
+            fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo.tentative, {&quot;FBTYPE&quot;: &quot;BUSY-TENTATIVE&quot;}))
+        if len(fbinfo.unavailable) != 0:
+            fb.addProperty(Property(&quot;FREEBUSY&quot;, fbinfo.unavailable, {&quot;FBTYPE&quot;: &quot;BUSY-UNAVAILABLE&quot;}))
+        if self.uid is not None:
+            fb.addProperty(Property(&quot;UID&quot;, self.uid))
+        else:
+            uid = str(uuid.uuid4())
+            fb.addProperty(Property(&quot;UID&quot;, uid))
+
+        if self.event_details:
+            for vevent in self.event_details:
+                fbcalendar.addComponent(vevent)
+
+        return fbcalendar
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -16,6 +16,7 @@
</span><span class="cx"> 
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="cx"> from pycalendar.duration import Duration
</span><ins>+from pycalendar.period import Period
</ins><span class="cx"> from pycalendar.timezone import Timezone
</span><span class="cx"> 
</span><span class="cx"> from twext.python.log import Logger
</span><span class="lines">@@ -23,13 +24,13 @@
</span><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> 
</span><del>-from twistedcaldav import customxml, caldavxml
</del><ins>+from twistedcaldav import customxml
</ins><span class="cx"> from twistedcaldav.accounting import emitAccounting, accountingEnabled
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.ical import Property
</span><span class="cx"> from twistedcaldav.instance import InvalidOverriddenInstanceError
</span><span class="cx"> 
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTipProcessing, iTIPRequestStatus, \
</span><span class="cx">     iTipGenerator
</span><span class="cx"> from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord
</span><span class="lines">@@ -799,6 +800,9 @@
</span><span class="cx">         # in any calendars.
</span><span class="cx">         uid = calendar.resourceUID()
</span><span class="cx"> 
</span><ins>+        # Object to do freebusy query
+        freebusy = FreebusyQuery(None, None, None, None, None, None, uid, None, accountingItems=accounting if len(instances) == 1 else None)
+
</ins><span class="cx">         # Now compare each instance time-range with the index and see if there is an overlap
</span><span class="cx">         fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
</span><span class="cx">         fbset = [fbcalendar for fbcalendar in fbset if fbcalendar.isUsedForFreeBusy()]
</span><span class="lines">@@ -822,7 +826,7 @@
</span><span class="cx">                 if instance.partstat == &quot;NEEDS-ACTION&quot; and instance.free and instance.active:
</span><span class="cx">                     try:
</span><span class="cx">                         # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
</span><del>-                        fbinfo = ([], [], [])
</del><ins>+                        fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx"> 
</span><span class="cx">                         def makeTimedUTC(dt):
</span><span class="cx">                             dt = dt.duplicate()
</span><span class="lines">@@ -834,18 +838,19 @@
</span><span class="cx">                                 dt.adjustToUTC()
</span><span class="cx">                             return dt
</span><span class="cx"> 
</span><del>-                        tr = caldavxml.TimeRange(
-                            start=str(makeTimedUTC(instance.start)),
-                            end=str(makeTimedUTC(instance.end)),
</del><ins>+                        tr = Period(
+                            makeTimedUTC(instance.start),
+                            makeTimedUTC(instance.end),
</ins><span class="cx">                         )
</span><span class="cx"> 
</span><del>-                        yield generateFreeBusyInfo(testcal, fbinfo, tr, 0, uid, servertoserver=True, accountingItems=accounting if len(instances) == 1 else None)
</del><ins>+                        freebusy.timerange = tr
+                        yield freebusy.generateFreeBusyInfo(testcal, fbinfo, 0)
</ins><span class="cx"> 
</span><span class="cx">                         # If any fbinfo entries exist we have an overlap
</span><del>-                        if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
</del><ins>+                        if len(fbinfo.busy) or len(fbinfo.tentative) or len(fbinfo.unavailable):
</ins><span class="cx">                             instance.free = False
</span><span class="cx">                         if accounting is not None:
</span><del>-                            accounting[&quot;tr&quot;].insert(0, (tr.attributes[&quot;start&quot;], tr.attributes[&quot;end&quot;], instance.free,))
</del><ins>+                            accounting[&quot;tr&quot;].insert(0, (tr.getStart().getText(), tr.getEnd().getText(), instance.free,))
</ins><span class="cx">                     except QueryMaxResources:
</span><span class="cx">                         instance.free[instance] = False
</span><span class="cx">                         log.info(&quot;Exceeded number of matches whilst trying to find free-time.&quot;)
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -42,6 +42,8 @@
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.delivery import ScheduleViaISchedule
</span><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
</span><span class="cx"> 
</span><ins>+from pycalendar.period import Period
+
</ins><span class="cx"> import hashlib
</span><span class="cx"> from collections import namedtuple
</span><span class="cx"> 
</span><span class="lines">@@ -400,9 +402,7 @@
</span><span class="cx">                         &quot;VFREEBUSY start or end not UTC&quot;,
</span><span class="cx">                     ))
</span><span class="cx"> 
</span><del>-                self.timeRange = caldavxml.TimeRange(start=dtstart.getText(), end=dtend.getText())
-                self.timeRange.start = dtstart
-                self.timeRange.end = dtend
</del><ins>+                self.timeRange = Period(dtstart, dtend)
</ins><span class="cx"> 
</span><span class="cx">                 # Look for masked UID
</span><span class="cx">                 self.excludeUID = self.calendar.getMaskUID()
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_freebusypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -22,11 +22,10 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks
</span><span class="cx"> from twisted.trial.unittest import TestCase
</span><span class="cx"> 
</span><del>-from twistedcaldav import caldavxml
</del><span class="cx"> from twistedcaldav.ical import Component, Property
</span><span class="cx"> 
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import buildFreeBusyResult, \
-    generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.common.datastore.test.util import CommonCommonTests, populateCalendarsFrom
</span><span class="cx"> 
</span><span class="cx"> def normalizeiCalendarText(data):
</span><span class="lines">@@ -46,13 +45,8 @@
</span><span class="cx">         data = (
</span><span class="cx">             (
</span><span class="cx">                 &quot;#1.1 No busy time&quot;,
</span><del>-                [
-                    [],
-                    [],
-                    [],
-                ],
-                &quot;20080601T000000Z&quot;,
-                &quot;20080602T000000Z&quot;,
</del><ins>+                FreebusyQuery.FBInfo([], [], []),
+                Period.parseText(&quot;20080601T000000Z/20080602T000000Z&quot;),
</ins><span class="cx">                 None,
</span><span class="cx">                 None,
</span><span class="cx">                 None,
</span><span class="lines">@@ -68,13 +62,8 @@
</span><span class="cx">             ),
</span><span class="cx">             (
</span><span class="cx">                 &quot;#1.2 No busy time with organizer &amp; attendee&quot;,
</span><del>-                [
-                    [],
-                    [],
-                    [],
-                ],
-                &quot;20080601T000000Z&quot;,
-                &quot;20080602T000000Z&quot;,
</del><ins>+                FreebusyQuery.FBInfo([], [], []),
+                Period.parseText(&quot;20080601T000000Z/20080602T000000Z&quot;),
</ins><span class="cx">                 Property(&quot;ORGANIZER&quot;, &quot;mailto:user01@example.com&quot;),
</span><span class="cx">                 Property(&quot;ATTENDEE&quot;, &quot;mailto:user02@example.com&quot;),
</span><span class="cx">                 None,
</span><span class="lines">@@ -92,13 +81,8 @@
</span><span class="cx">             ),
</span><span class="cx">             (
</span><span class="cx">                 &quot;#1.3 With single busy time&quot;,
</span><del>-                [
-                    [Period.parseText(&quot;20080601T120000Z/20080601T130000Z&quot;), ],
-                    [],
-                    [],
-                ],
-                &quot;20080601T000000Z&quot;,
-                &quot;20080602T000000Z&quot;,
</del><ins>+                FreebusyQuery.FBInfo([Period.parseText(&quot;20080601T120000Z/20080601T130000Z&quot;), ], [], []),
+                Period.parseText(&quot;20080601T000000Z/20080602T000000Z&quot;),
</ins><span class="cx">                 Property(&quot;ORGANIZER&quot;, &quot;mailto:user01@example.com&quot;),
</span><span class="cx">                 Property(&quot;ATTENDEE&quot;, &quot;mailto:user02@example.com&quot;),
</span><span class="cx">                 None,
</span><span class="lines">@@ -117,16 +101,15 @@
</span><span class="cx">             ),
</span><span class="cx">             (
</span><span class="cx">                 &quot;#1.4 With multiple busy time&quot;,
</span><del>-                [
</del><ins>+                FreebusyQuery.FBInfo(
</ins><span class="cx">                     [
</span><span class="cx">                         Period.parseText(&quot;20080601T120000Z/20080601T130000Z&quot;),
</span><span class="cx">                         Period.parseText(&quot;20080601T140000Z/20080601T150000Z&quot;),
</span><span class="cx">                     ],
</span><span class="cx">                     [],
</span><span class="cx">                     [],
</span><del>-                ],
-                &quot;20080601T000000Z&quot;,
-                &quot;20080602T000000Z&quot;,
</del><ins>+                ),
+                Period.parseText(&quot;20080601T000000Z/20080602T000000Z&quot;),
</ins><span class="cx">                 Property(&quot;ORGANIZER&quot;, &quot;mailto:user01@example.com&quot;),
</span><span class="cx">                 Property(&quot;ATTENDEE&quot;, &quot;mailto:user02@example.com&quot;),
</span><span class="cx">                 None,
</span><span class="lines">@@ -145,7 +128,7 @@
</span><span class="cx">             ),
</span><span class="cx">             (
</span><span class="cx">                 &quot;#1.5 With multiple busy time, some overlap&quot;,
</span><del>-                [
</del><ins>+                FreebusyQuery.FBInfo(
</ins><span class="cx">                     [
</span><span class="cx">                         Period.parseText(&quot;20080601T120000Z/20080601T130000Z&quot;),
</span><span class="cx">                         Period.parseText(&quot;20080601T123000Z/20080601T133000Z&quot;),
</span><span class="lines">@@ -154,9 +137,8 @@
</span><span class="cx">                     ],
</span><span class="cx">                     [],
</span><span class="cx">                     [],
</span><del>-                ],
-                &quot;20080601T000000Z&quot;,
-                &quot;20080602T000000Z&quot;,
</del><ins>+                ),
+                Period.parseText(&quot;20080601T000000Z/20080602T000000Z&quot;),
</ins><span class="cx">                 Property(&quot;ORGANIZER&quot;, &quot;mailto:user01@example.com&quot;),
</span><span class="cx">                 Property(&quot;ATTENDEE&quot;, &quot;mailto:user02@example.com&quot;),
</span><span class="cx">                 None,
</span><span class="lines">@@ -175,7 +157,7 @@
</span><span class="cx">             ),
</span><span class="cx">             (
</span><span class="cx">                 &quot;#1.6 With all busy time types&quot;,
</span><del>-                [
</del><ins>+                FreebusyQuery.FBInfo(
</ins><span class="cx">                     [
</span><span class="cx">                         Period.parseText(&quot;20080601T120000Z/20080601T130000Z&quot;),
</span><span class="cx">                         Period.parseText(&quot;20080601T140000Z/20080601T150000Z&quot;),
</span><span class="lines">@@ -186,9 +168,8 @@
</span><span class="cx">                     [
</span><span class="cx">                         Period.parseText(&quot;20080601T160000Z/20080601T170000Z&quot;),
</span><span class="cx">                     ],
</span><del>-                ],
-                &quot;20080601T000000Z&quot;,
-                &quot;20080602T000000Z&quot;,
</del><ins>+                ),
+                Period.parseText(&quot;20080601T000000Z/20080602T000000Z&quot;),
</ins><span class="cx">                 Property(&quot;ORGANIZER&quot;, &quot;mailto:user01@example.com&quot;),
</span><span class="cx">                 Property(&quot;ATTENDEE&quot;, &quot;mailto:user02@example.com&quot;),
</span><span class="cx">                 None,
</span><span class="lines">@@ -209,13 +190,12 @@
</span><span class="cx">             ),
</span><span class="cx">             (
</span><span class="cx">                 &quot;#1.7 With single busy time and event details&quot;,
</span><del>-                [
</del><ins>+                FreebusyQuery.FBInfo(
</ins><span class="cx">                     [Period.parseText(&quot;20080601T120000Z/20080601T130000Z&quot;), ],
</span><span class="cx">                     [],
</span><span class="cx">                     [],
</span><del>-                ],
-                &quot;20080601T000000Z&quot;,
-                &quot;20080602T000000Z&quot;,
</del><ins>+                ),
+                Period.parseText(&quot;20080601T000000Z/20080602T000000Z&quot;),
</ins><span class="cx">                 Property(&quot;ORGANIZER&quot;, &quot;mailto:user01@example.com&quot;),
</span><span class="cx">                 Property(&quot;ATTENDEE&quot;, &quot;mailto:user02@example.com&quot;),
</span><span class="cx">                 [
</span><span class="lines">@@ -250,16 +230,16 @@
</span><span class="cx">             ),
</span><span class="cx">         )
</span><span class="cx"> 
</span><del>-        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)
</del><ins>+        for description, fbinfo, timerange, organizer, attendee, event_details, calendar in data:
+            freebusy = FreebusyQuery(None, organizer, None, attendee, None, timerange, None, None, event_details=event_details)
+            result = freebusy.buildFreeBusyResult(fbinfo)
</ins><span class="cx">             self.assertEqual(normalizeiCalendarText(str(result)), calendar.replace(&quot;\n&quot;, &quot;\r\n&quot;), msg=description)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class GenerateFreeBusyInfo(CommonCommonTests, TestCase):
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    Test txdav.caldav.datastore.scheduling.freebusy.generateFreeBusyInfo
</del><ins>+    Test txdav.caldav.datastore.scheduling.freebusy.FreebusyQuery
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -332,14 +312,16 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         calendar = (yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar_1&quot;))
</span><del>-        fbinfo = [[], [], [], ]
</del><ins>+        fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx">         matchtotal = 0
</span><del>-        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
-        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
</del><ins>+        timerange = Period(self.now, self.now_1D)
+
+        freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
+        result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
</ins><span class="cx">         self.assertEqual(result, 0)
</span><del>-        self.assertEqual(len(fbinfo[0]), 0)
-        self.assertEqual(len(fbinfo[1]), 0)
-        self.assertEqual(len(fbinfo[2]), 0)
</del><ins>+        self.assertEqual(len(fbinfo.busy), 0)
+        self.assertEqual(len(fbinfo.tentative), 0)
+        self.assertEqual(len(fbinfo.unavailable), 0)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -362,14 +344,16 @@
</span><span class="cx"> 
</span><span class="cx">         yield self._createCalendarObject(data, &quot;user01&quot;, &quot;test.ics&quot;)
</span><span class="cx">         calendar = (yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar_1&quot;))
</span><del>-        fbinfo = [[], [], [], ]
</del><ins>+        fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx">         matchtotal = 0
</span><del>-        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
-        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
</del><ins>+        timerange = Period(self.now, self.now_1D)
+
+        freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
+        result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
</ins><span class="cx">         self.assertEqual(result, 1)
</span><del>-        self.assertEqual(fbinfo[0], [Period.parseText(&quot;%s/%s&quot; % (self.now_12H.getText(), self.now_13H.getText(),)), ])
-        self.assertEqual(len(fbinfo[1]), 0)
-        self.assertEqual(len(fbinfo[2]), 0)
</del><ins>+        self.assertEqual(fbinfo.busy, [Period.parseText(&quot;%s/%s&quot; % (self.now_12H.getText(), self.now_13H.getText(),)), ])
+        self.assertEqual(len(fbinfo.tentative), 0)
+        self.assertEqual(len(fbinfo.unavailable), 0)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -392,21 +376,23 @@
</span><span class="cx"> 
</span><span class="cx">         yield self._createCalendarObject(data, &quot;user01&quot;, &quot;test.ics&quot;)
</span><span class="cx">         calendar = (yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar_1&quot;))
</span><del>-        fbinfo = [[], [], [], ]
</del><ins>+        fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx">         matchtotal = 0
</span><del>-        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
</del><ins>+        timerange = Period(self.now, self.now_1D)
</ins><span class="cx">         event_details = []
</span><del>-        result = (yield generateFreeBusyInfo(
</del><ins>+
+        organizer = recipient = (yield calendarUserFromCalendarUserAddress(&quot;mailto:user01@example.com&quot;, self.transactionUnderTest()))
+
+        freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None, event_details=event_details)
+        freebusy.same_calendar_user = True
+        result = (yield freebusy.generateFreeBusyInfo(
</ins><span class="cx">             calendar,
</span><span class="cx">             fbinfo,
</span><del>-            timerange,
</del><span class="cx">             matchtotal,
</span><del>-            organizer=&quot;mailto:user01@example.com&quot;,
-            event_details=event_details
</del><span class="cx">         ))
</span><span class="cx">         self.assertEqual(result, 1)
</span><del>-        self.assertEqual(fbinfo[0], [Period.parseText(&quot;%s/%s&quot; % (self.now_12H.getText(), self.now_13H.getText(),)), ])
-        self.assertEqual(len(fbinfo[1]), 0)
-        self.assertEqual(len(fbinfo[2]), 0)
</del><ins>+        self.assertEqual(fbinfo.busy, [Period(self.now_12H, self.now_13H), ])
+        self.assertEqual(len(fbinfo.tentative), 0)
+        self.assertEqual(len(fbinfo.unavailable), 0)
</ins><span class="cx">         self.assertEqual(len(event_details), 1)
</span><span class="cx">         self.assertEqual(str(event_details[0]), str(tuple(Component.fromString(data).subcomponents())[0]))
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastorepoddingstore_apipy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/podding/store_api.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/podding/store_api.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/common/datastore/podding/store_api.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -16,11 +16,13 @@
</span><span class="cx"> 
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> 
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.common.datastore.podding.util import UtilityConduitMixin
</span><span class="cx"> from txdav.common.datastore.sql_tables import _HOME_STATUS_DISABLED
</span><span class="cx"> 
</span><del>-from twistedcaldav.caldavxml import TimeRange
</del><ins>+from pycalendar.period import Period
+
</ins><span class="cx"> from datetime import datetime
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -69,13 +71,11 @@
</span><span class="cx">     def send_freebusy(
</span><span class="cx">         self,
</span><span class="cx">         calresource,
</span><ins>+        organizer,
+        recipient,
</ins><span class="cx">         timerange,
</span><span class="cx">         matchtotal,
</span><span class="cx">         excludeuid,
</span><del>-        organizer,
-        organizerPrincipal,
-        same_calendar_user,
-        servertoserver,
</del><span class="cx">         event_details,
</span><span class="cx">     ):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -84,13 +84,11 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         txn, request, server = yield self._getRequestForStoreObject(&quot;freebusy&quot;, calresource, False)
</span><span class="cx"> 
</span><del>-        request[&quot;timerange&quot;] = [timerange.start.getText(), timerange.end.getText()]
</del><ins>+        request[&quot;organizer&quot;] = organizer
+        request[&quot;recipient&quot;] = organizer
+        request[&quot;timerange&quot;] = timerange.getText()
</ins><span class="cx">         request[&quot;matchtotal&quot;] = matchtotal
</span><span class="cx">         request[&quot;excludeuid&quot;] = excludeuid
</span><del>-        request[&quot;organizer&quot;] = organizer
-        request[&quot;organizerPrincipal&quot;] = organizerPrincipal
-        request[&quot;same_calendar_user&quot;] = same_calendar_user
-        request[&quot;servertoserver&quot;] = servertoserver
</del><span class="cx">         request[&quot;event_details&quot;] = event_details
</span><span class="cx"> 
</span><span class="cx">         response = yield self.sendRequestToServer(txn, server, request)
</span><span class="lines">@@ -109,28 +107,26 @@
</span><span class="cx">         # Operate on the L{CommonHomeChild}
</span><span class="cx">         calresource, _ignore = yield self._getStoreObjectForRequest(txn, request)
</span><span class="cx"> 
</span><del>-        fbinfo = [[], [], []]
-        matchtotal = yield generateFreeBusyInfo(
</del><ins>+        organizer = yield calendarUserFromCalendarUserAddress(request[&quot;organizer&quot;], txn)
+        recipient = yield calendarUserFromCalendarUserAddress(request[&quot;recipient&quot;], txn)
+
+        freebusy = FreebusyQuery(
+            organizer, None, recipient, None, None,
+            Period.parseText(request[&quot;timerange&quot;]), request[&quot;excludeuid&quot;], None, event_details=request[&quot;event_details&quot;])
+        fbinfo = FreebusyQuery.FBInfo([], [], [])
+        matchtotal = yield freebusy.generateFreeBusyInfo(
</ins><span class="cx">             calresource,
</span><span class="cx">             fbinfo,
</span><del>-            TimeRange(start=request[&quot;timerange&quot;][0], end=request[&quot;timerange&quot;][1]),
</del><span class="cx">             request[&quot;matchtotal&quot;],
</span><del>-            request[&quot;excludeuid&quot;],
-            request[&quot;organizer&quot;],
-            request[&quot;organizerPrincipal&quot;],
-            request[&quot;same_calendar_user&quot;],
-            request[&quot;servertoserver&quot;],
-            request[&quot;event_details&quot;],
-            logItems=None
</del><span class="cx">         )
</span><span class="cx"> 
</span><del>-        # Convert L{DateTime} objects to text for JSON response
-        for i in range(3):
-            for j in range(len(fbinfo[i])):
-                fbinfo[i][j] = fbinfo[i][j].getText()
-
</del><ins>+        # Convert L{Period} objects to text for JSON response
</ins><span class="cx">         returnValue({
</span><del>-            &quot;fbresults&quot;: fbinfo,
</del><ins>+            &quot;fbresults&quot;: [
+                [item.getText() for item in fbinfo.busy],
+                [item.getText() for item in fbinfo.tentative],
+                [item.getText() for item in fbinfo.unavailable],
+            ],
</ins><span class="cx">             &quot;matchtotal&quot;: matchtotal,
</span><span class="cx">         })
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastorepoddingtesttest_conduitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -26,11 +26,11 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, succeed, returnValue
</span><span class="cx"> 
</span><span class="cx"> from twistedcaldav import caldavxml
</span><del>-from twistedcaldav.caldavxml import TimeRange
</del><span class="cx"> from twistedcaldav.ical import Component, normalize_iCalStr
</span><span class="cx"> 
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.localservers import ServersDB, Server
</span><span class="cx"> from txdav.caldav.datastore.sql import ManagedAttachment, AttachmentLink
</span><span class="cx"> from txdav.caldav.datastore.test.common import CaptureProtocol
</span><span class="lines">@@ -927,21 +927,13 @@
</span><span class="cx"> 
</span><span class="cx">         shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home=&quot;puser01&quot;, name=&quot;shared-calendar&quot;)
</span><span class="cx"> 
</span><del>-        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
-        )
</del><ins>+        fbinfo = FreebusyQuery.FBInfo([], [], [])
+        timerange = Period(DateTime.parseText(fbstart), DateTime.parseText(fbend))
+        organizer = recipient = (yield calendarUserFromCalendarUserAddress(&quot;mailto:puser01@example.com&quot;, self.theTransactionUnderTest(1)))
</ins><span class="cx"> 
</span><ins>+        freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None)
+        matchtotal = (yield freebusy.generateFreeBusyInfo(shared, fbinfo, 0))
+
</ins><span class="cx">         self.assertEqual(matchtotal, 1)
</span><span class="cx">         self.assertEqual(fbinfo[0], [Period.parseText(&quot;{now:04d}0102T140000Z/PT1H&quot;.format(**self.nowYear)), ])
</span><span class="cx">         self.assertEqual(len(fbinfo[1]), 0)
</span></span></pre>
</div>
</div>

</body>
</html>