[CalendarServer-changes] [14688] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Apr 20 11:00:40 PDT 2015


Revision: 14688
          http://trac.calendarserver.org//changeset/14688
Author:   cdaboo at apple.com
Date:     2015-04-20 11:00:40 -0700 (Mon, 20 Apr 2015)
Log Message:
-----------
Re-factor free busy to push fbset loop lower down in the call stack.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/freebusyurl.py
    CalendarServer/trunk/twistedcaldav/method/report_freebusy.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py
    CalendarServer/trunk/txdav/common/datastore/podding/store_api.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py

Modified: CalendarServer/trunk/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/freebusyurl.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/twistedcaldav/freebusyurl.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -270,10 +270,12 @@
         attendeeProp = Property("ATTENDEE", recipient.cuaddr)
         timerange = Period(self.start, self.end)
 
-        fbresult = (yield FreebusyQuery(
-            organizer, None, recipient, attendeeProp, None,
-            timerange, None, None,
-        ).generateAttendeeFreeBusyResponse())
+        fbresult = yield FreebusyQuery(
+            organizer=organizer,
+            recipient=recipient,
+            attendeeProp=attendeeProp,
+            timerange=timerange,
+        ).generateAttendeeFreeBusyResponse()
 
         response = Response()
         response.stream = MemoryStream(str(fbresult))

Modified: CalendarServer/trunk/twistedcaldav/method/report_freebusy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_freebusy.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/twistedcaldav/method/report_freebusy.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -37,6 +37,7 @@
 from twistedcaldav.util import bestAcceptType
 
 from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
 from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
 from txdav.xml import element as davxml
 
@@ -85,12 +86,11 @@
     yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),))
 
     # Do the actual freebusy query against the set of matched calendars
+    principal = yield self.ownerPrincipal(request)
+    organizer = recipient = LocalCalendarUser(principal.canonicalCalendarUserAddress(), principal.record)
     timerange = Period(timerange.start, timerange.end)
     try:
-        fbresult = (yield FreebusyQuery(
-            None, None, None, None, None,
-            timerange, None, None,
-        ).generateAttendeeFreeBusyResponse(fbset=fbset, method=None))
+        fbresult = yield FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange).generateAttendeeFreeBusyResponse(fbset=fbset, method=None)
     except NumberOfMatchesWithinLimits:
         log.error("Too many matching components in free-busy report")
         raise HTTPError(ErrorResponse(

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -200,11 +200,17 @@
         attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
 
         try:
-            fbresult = (yield FreebusyQuery(
-                self.scheduler.organizer, organizerProp, recipient, attendeeProp, uid,
-                self.scheduler.timeRange, self.scheduler.excludeUID, self.scheduler.logItems,
+            fbresult = yield FreebusyQuery(
+                organizer=self.scheduler.organizer,
+                organizerProp=organizerProp,
+                recipient=recipient,
+                attendeeProp=attendeeProp,
+                uid=uid,
+                timerange=self.scheduler.timeRange,
+                excludeUID=self.scheduler.excludeUID,
+                logItems=self.scheduler.logItems,
                 event_details=event_details,
-            ).generateAttendeeFreeBusyResponse())
+            ).generateAttendeeFreeBusyResponse()
         except Exception as e:
             log.failure(
                 "Could not determine free busy information for recipient {cuaddr}",

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -103,7 +103,11 @@
     FBInfo_mapper = {"BUSY": "busy", "BUSY-TENTATIVE": "tentative", "BUSY-UNAVAILABLE": "unavailable"}
     FBInfo_index_mapper = {'B': "busy", 'T': "tentative", 'U': "unavailable"}
 
-    def __init__(self, organizer, organizerProp, recipient, attendeeProp, uid, timeRange, excludeUID, logItems, accountingItems=None, event_details=None):
+    def __init__(
+        self,
+        organizer=None, organizerProp=None, recipient=None, attendeeProp=None,
+        uid=None, timerange=None, excludeUID=None, logItems=None, accountingItems=None, event_details=None
+    ):
         """
 
         @param organizer: the organizer making the freebusy request
@@ -114,10 +118,13 @@
         @type recipient: L{CalendarUser}
         @param attendeeProp: iCalendar ATTENDEE property from the request
         @type attendeeProp: L{Property}
+        @param authzuid: directory UID of the currently authenticated user (i.e., the one making
+            the actual free busy request - might be different from the organizer)
+        @type authzuid: L{str}
         @param uid: iCalendar UID in the request
         @type uid: L{str}
-        @param timeRange: time range for freebusy request
-        @type timeRange: L{Period}
+        @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
@@ -132,7 +139,7 @@
         self.recipient = recipient
         self.attendeeProp = attendeeProp
         self.uid = uid
-        self.timerange = timeRange
+        self.timerange = timerange
         self.excludeuid = excludeUID
         self.logItems = logItems
         self.accountingItems = accountingItems
@@ -145,8 +152,50 @@
         else:
             self.same_calendar_user = False
 
+        # May need organizer principal
+        self.organizer_record = self.organizer.record if self.organizer and self.organizer.hosted() else None
+        self.organizer_uid = self.organizer_record.uid if self.organizer_record else None
 
+        # Free busy is per-user
+        self.attendee_record = self.recipient.record if self.recipient and self.recipient.hosted() else None
+        self.attendee_uid = self.attendee_record.uid if self.attendee_record else None
+
+
     @inlineCallbacks
+    def checkRichOptions(self, txn):
+        if not hasattr(self, "rich_options"):
+            # Look for possible extended free busy information
+            self.rich_options = {
+                "organizer": False,
+                "delegate": False,
+                "resource": False,
+            }
+            if self.event_details is not None and self.organizer_record is not None and self.attendee_record is not None:
+
+                # Get the principal of the authorized user which may be different from the organizer if a delegate of
+                # the organizer is making the request
+                authzuid = txn._authz_uid if txn is not None else None
+                if authzuid is not None and authzuid != self.organizer_uid:
+                    authz_record = yield txn.directoryService().recordWithUID(authzuid.decode("utf-8"))
+                else:
+                    self.authzuid = self.organizer_uid
+                    authz_record = self.organizer_record
+
+                # Check if attendee is also the organizer or the delegate doing the request
+                if self.attendee_uid in (self.organizer_uid, authzuid):
+                    self.rich_options["organizer"] = True
+
+                # Check if authorized user is a delegate of attendee
+                proxy = (yield authz_record.isProxyFor(self.attendee_record))
+                if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
+                    self.rich_options["delegate"] = True
+
+                # Check if attendee is room or resource
+                if config.Scheduling.Options.RoomResourceRichFreeBusy and self.attendee_record.getCUType() in ("RESOURCE", "ROOM",):
+                    self.rich_options["resource"] = True
+
+
+    @inlineCallbacks
     def generateAttendeeFreeBusyResponse(self, fbset=None, method="REPLY"):
 
         # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
@@ -159,18 +208,13 @@
                 fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]
 
             # Process the availability property from the Inbox.
-            availability = self.recipient.inbox.ownerHome().getAvailability()
-            if availability is not None:
-                self.processAvailabilityFreeBusy(availability, fbinfo)
+            if hasattr(self.recipient, "inbox"):
+                availability = self.recipient.inbox.ownerHome().getAvailability()
+                if availability is not None:
+                    self.processAvailabilityFreeBusy(availability, fbinfo)
 
         # Now process free-busy set calendars
-        matchtotal = 0
-        for calendar in fbset:
-            matchtotal = (yield self.generateFreeBusyInfo(
-                calendar,
-                fbinfo,
-                matchtotal,
-            ))
+        yield self.generateFreeBusyInfo(fbset, fbinfo)
 
         # Build VFREEBUSY iTIP reply for this recipient
         fbresult = self.buildFreeBusyResult(fbinfo, method=method)
@@ -268,50 +312,63 @@
         return periods
 
 
-    def generateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+    @inlineCallbacks
+    def generateFreeBusyInfo(self, fbset, fbinfo, matchtotal=0):
         """
         Get freebusy information for a calendar. Different behavior for internal vs external calendars.
 
         See L{_internalGenerateFreeBusyInfo} for argument description.
         """
 
-        # 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,
+        # Split calendar set into internal/external items
+        fbset_internal = [calendar for calendar in fbset if not calendar.external()]
+        fbset_external = [calendar for calendar in fbset if calendar.external()]
+
+        # TODO: we should probably figure out how to run the internal and external ones in parallel
+        if fbset_external:
+            matchtotal += (yield self._externalGenerateFreeBusyInfo(
+                fbset_external,
                 fbinfo,
                 matchtotal,
-            )
-        else:
-            return self._internalGenerateFreeBusyInfo(
-                calresource,
+            ))
+
+        if fbset_internal:
+            matchtotal += (yield self._internalGenerateFreeBusyInfo(
+                fbset_internal,
                 fbinfo,
                 matchtotal,
-            )
+            ))
 
+        returnValue(matchtotal)
 
+
     @inlineCallbacks
-    def _externalGenerateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+    def _externalGenerateFreeBusyInfo(self, fbset, fbinfo, matchtotal):
         """
         Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
         any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
 
         See L{_internalGenerateFreeBusyInfo} for argument description.
         """
-        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]])
+        for calresource in fbset:
+            fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(
+                calresource,
+                self.organizer.cuaddr if self.organizer else None,
+                self.recipient.cuaddr if self.recipient else None,
+                self.timerange,
+                matchtotal,
+                self.excludeuid,
+                self.event_details,
+            )
+            for i in range(3):
+                fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
         returnValue(matchtotal)
 
 
     @inlineCallbacks
     def _internalGenerateFreeBusyInfo(
         self,
-        calresource,
+        fbset,
         fbinfo,
         matchtotal,
     ):
@@ -323,141 +380,34 @@
         @param matchtotal:  the running total for the number of matches.
         """
 
-        # 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
+        yield self.checkRichOptions(fbset[0]._txn)
 
-        # 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 ""
+        calidmap = dict([(fbcalendar.id(), fbcalendar,) for fbcalendar in fbset])
+        directoryService = fbset[0].directoryService()
 
-        # 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()
+        resources, tzinfos = yield self._matchResources(fbset)
 
-        # Get the timezone property from the collection.
-        tz = calresource.getTimezone()
-
-        # Look for possible extended free busy information
-        rich_options = {
-            "organizer": False,
-            "delegate": False,
-            "resource": False,
-        }
-        do_event_details = False
-        if self.event_details is not None and organizer_record is not None and attendee_record is not None:
-
-            # Get the principal of the authorized user which may be different from the organizer if a delegate of
-            # the organizer is making the request
-            authz_uid = organizer_uid
-            authz_record = organizer_record
-            if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid:
-                authz_uid = calresource._txn._authz_uid
-                authz_record = yield calresource.directoryService().recordWithUID(authz_uid.decode("utf-8"))
-
-            # Check if attendee is also the organizer or the delegate doing the request
-            if attendee_uid in (organizer_uid, authz_uid):
-                do_event_details = True
-                rich_options["organizer"] = True
-
-            # Check if authorized user is a delegate of attendee
-            proxy = (yield authz_record.isProxyFor(attendee_record))
-            if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
-                do_event_details = True
-                rich_options["delegate"] = True
-
-            # Check if attendee is room or resource
-            if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in ("RESOURCE", "ROOM",):
-                do_event_details = True
-                rich_options["resource"] = True
-
-        # Try cache
-        resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None
-
-        if resources is None:
-
-            if self.accountingItems is not None:
-                self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1
-
-            caching = False
-            if config.EnableFreeBusyCache:
-                # Log extended item
-                if self.logItems is not None:
-                    self.logItems["fb-uncached"] = self.logItems.get("fb-uncached", 0) + 1
-
-                # We want to cache a large range of time based on the current date
-                cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
-                cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
-
-                # If the requested time range would fit in our allowed cache range, trigger the cache creation
-                if compareDateTime(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0:
-                    cache_timerange = Period(cache_start, cache_end)
-                    caching = True
-
-            #
-            # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
-            # We then take those results and merge them into one VFREEBUSY component
-            # with appropriate FREEBUSY properties, and return that single item as iCal data.
-            #
-
-            # Create fake filter element to match time-range
-            tr = TimeRange(
-                start=(cache_timerange if caching else self.timerange).getStart().getText(),
-                end=(cache_timerange if caching else self.timerange).getEnd().getText(),
-            )
-            filter = caldavxml.Filter(
-                caldavxml.ComponentFilter(
-                    caldavxml.ComponentFilter(
-                        tr,
-                        name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
-                    ),
-                    name="VCALENDAR",
-                )
-            )
-            filter = Filter(filter)
-            tzinfo = filter.settimezone(tz)
-            if self.accountingItems is not None:
-                self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),)
-
-            try:
-                resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True)
-                if caching:
-                    yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
-            except IndexedSearchException:
-                raise InternalDataStoreError("Invalid indexedSearch query")
-
-        else:
-            if self.accountingItems is not None:
-                self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1
-
-            # Log extended item
-            if self.logItems is not None:
-                self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1
-
-            # Determine appropriate timezone (UTC is the default)
-            tzinfo = tz.gettimezone() if tz is not None else Timezone(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:
+        for calid, 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,))
+            aggregated_resources.setdefault((calid, name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
 
         if self.accountingItems is not None:
             self.accountingItems["fb-resources"] = {}
             for k, v in aggregated_resources.items():
-                name, uid, type, test_organizer = k
+                calid, name, uid, type, test_organizer = k
                 self.accountingItems["fb-resources"][uid] = []
                 for float, start, end, fbtype in v:
                     fbstart = parseSQLTimestampToPyCalendar(start)
                     if float == 'Y':
-                        fbstart.setTimezone(tzinfo)
+                        fbstart.setTimezone(tzinfos[calid])
                     else:
                         fbstart.setTimezone(Timezone(utc=True))
                     fbend = parseSQLTimestampToPyCalendar(end)
                     if float == 'Y':
-                        fbend.setTimezone(tzinfo)
+                        fbend.setTimezone(tzinfos[calid])
                     else:
                         fbend.setTimezone(Timezone(utc=True))
                     self.accountingItems["fb-resources"][uid].append((
@@ -472,7 +422,9 @@
         recordUIDCache = {}
         for key in aggregated_resources.iterkeys():
 
-            name, uid, type, test_organizer = key
+            calid, name, uid, type, test_organizer = key
+            calresource = calidmap[calid]
+            tzinfo = tzinfos[calid]
 
             # Short-cut - if an fbtype exists we can use that
             if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
@@ -486,25 +438,9 @@
                         continue
 
                     # 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 ""
-                                    recordUIDCache[test_organizer] = test_uid
-                            else:
-                                test_uid = ""
+                    if (yield self._testIgnoreExcludeUID(uid, test_organizer, recordUIDCache, directoryService)):
+                        continue
 
-                            # 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 == "") and self.same_calendar_user:
-                                continue
-
                     # Apply a timezone to any floating times
                     fbstart = parseSQLTimestampToPyCalendar(start)
                     if float == 'Y':
@@ -532,12 +468,12 @@
                         raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
 
                     # Add extended details
-                    if do_event_details:
+                    if any(self.rich_options.values()):
                         child = (yield calresource.calendarObjectWithName(name))
                         # Only add fully public events
                         if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
                             calendar = (yield child.componentForUser())
-                            self._addEventDetails(calendar, rich_options, tzinfo)
+                            self._addEventDetails(calendar, self.rich_options, tzinfo)
 
             else:
                 child = (yield calresource.calendarObjectWithName(name))
@@ -551,26 +487,9 @@
                     continue
 
                 # 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 ""
-                                recordUIDCache[test_organizer] = test_uid
-                        else:
-                            test_uid = ""
+                if (yield self._testIgnoreExcludeUID(uid, calendar.getOrganizer(), recordUIDCache, calresource.directoryService())):
+                    continue
 
-                        # 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
-
                 if self.accountingItems is not None:
                     self.accountingItems.setdefault("fb-filter-match", []).append(uid)
 
@@ -593,16 +512,146 @@
                         assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
 
                     # Add extended details
-                    if calendar.mainType() == "VEVENT" and do_event_details:
-                        child = (yield calresource.calendarObjectWithName(name))
+                    if calendar.mainType() == "VEVENT" and any(self.rich_options.values()):
                         # Only add fully public events
                         if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
-                            calendar = (yield child.componentForUser())
-                            self._addEventDetails(calendar, rich_options, tzinfo)
+                            self._addEventDetails(calendar, self.rich_options, tzinfo)
 
         returnValue(matchtotal)
 
 
+    @inlineCallbacks
+    def _matchResources(self, fbset):
+        """
+        For now iterate over each calendar and collect the results. In the longer term we might want to consider
+        doing a single DB query in the case where multiple calendars need to be searched.
+
+        @param fbset: list of calendars to process
+        @type fbset: L{list} of L{Calendar}
+        """
+
+        results = []
+        tzinfos = {}
+        for calresource in fbset:
+            resources, tzinfo = yield self._matchCalendarResources(calresource)
+            results.extend([(calresource.id(),) + tuple(resource) for resource in resources])
+            tzinfos[calresource.id()] = tzinfo
+
+        returnValue((results, tzinfos,))
+
+
+    @inlineCallbacks
+    def _matchCalendarResources(self, calresource):
+
+        # Get the timezone property from the collection.
+        tz = calresource.getTimezone()
+
+        # Try cache
+        resources = (yield FBCacheEntry.getCacheEntry(calresource, self.attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None
+
+        if resources is None:
+
+            if self.accountingItems is not None:
+                self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1
+
+            caching = False
+            if config.EnableFreeBusyCache:
+                # Log extended item
+                if self.logItems is not None:
+                    self.logItems["fb-uncached"] = self.logItems.get("fb-uncached", 0) + 1
+
+                # We want to cache a large range of time based on the current date
+                cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
+                cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
+
+                # If the requested time range would fit in our allowed cache range, trigger the cache creation
+                if compareDateTime(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0:
+                    cache_timerange = Period(cache_start, cache_end)
+                    caching = True
+
+            #
+            # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
+            # We then take those results and merge them into one VFREEBUSY component
+            # with appropriate FREEBUSY properties, and return that single item as iCal data.
+            #
+
+            # Create fake filter element to match time-range
+            tr = TimeRange(
+                start=(cache_timerange if caching else self.timerange).getStart().getText(),
+                end=(cache_timerange if caching else self.timerange).getEnd().getText(),
+            )
+            filter = caldavxml.Filter(
+                caldavxml.ComponentFilter(
+                    caldavxml.ComponentFilter(
+                        tr,
+                        name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+                    ),
+                    name="VCALENDAR",
+                )
+            )
+            filter = Filter(filter)
+            tzinfo = filter.settimezone(tz)
+            if self.accountingItems is not None:
+                self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),)
+
+            try:
+                resources = yield calresource.search(filter, useruid=self.attendee_uid, fbtype=True)
+                if caching:
+                    yield FBCacheEntry.makeCacheEntry(calresource, self.attendee_uid, cache_timerange, resources)
+            except IndexedSearchException:
+                raise InternalDataStoreError("Invalid indexedSearch query")
+
+        else:
+            if self.accountingItems is not None:
+                self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1
+
+            # Log extended item
+            if self.logItems is not None:
+                self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1
+
+            # Determine appropriate timezone (UTC is the default)
+            tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
+
+        returnValue((resources, tzinfo,))
+
+
+    @inlineCallbacks
+    def _testIgnoreExcludeUID(self, uid, test_organizer, recordUIDCache, dirservice):
+        """
+        Check whether the event with the specified UID can be correctly excluded from the
+        freebusy result.
+
+        @param uid: UID to test
+        @type uid: L{str}
+        @param test_organizer: organizer cu-address of the event
+        @type test_organizer: L{str}
+        @param recordUIDCache: cache of directory records
+        @type recordUIDCache: L{dict}
+        @param dirservice: directory service to use for record lookups
+        @type dirservice: L{DirectoryService}
+        """
+
+        # 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 dirservice.recordWithCalendarUserAddress(test_organizer))
+                    test_uid = test_record.uid if test_record else ""
+                    recordUIDCache[test_organizer] = test_uid
+            else:
+                test_uid = ""
+
+            # Check that ORGANIZER's match (security requirement)
+            if (self.organizer is None) or (self.organizer_uid == test_uid):
+                returnValue(True)
+            # Check for no ORGANIZER and check by same calendar user
+            elif (test_uid == "") and self.same_calendar_user:
+                returnValue(True)
+
+        returnValue(False)
+
+
     def _addEventDetails(self, calendar, rich_options, tzinfo):
         """
         Expand events within the specified time range and limit the set of properties to those allowed for

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -801,7 +801,7 @@
         uid = calendar.resourceUID()
 
         # Object to do freebusy query
-        freebusy = FreebusyQuery(None, None, None, None, None, None, uid, None, accountingItems=accounting if len(instances) == 1 else None)
+        freebusy = FreebusyQuery(recipient=self.recipient, excludeUID=uid, accountingItems=accounting if len(instances) == 1 else None)
 
         # Now compare each instance time-range with the index and see if there is an overlap
         fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
@@ -844,7 +844,7 @@
                         )
 
                         freebusy.timerange = tr
-                        yield freebusy.generateFreeBusyInfo(testcal, fbinfo, 0)
+                        yield freebusy.generateFreeBusyInfo([testcal, ], fbinfo)
 
                         # If any fbinfo entries exist we have an overlap
                         if len(fbinfo.busy) or len(fbinfo.tentative) or len(fbinfo.unavailable):

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -61,7 +61,7 @@
 """,
             ),
             (
-                "#1.2 No busy time with organizer & attendee",
+                "#1.2 No busy time with organizerProp & attendeeProp",
                 FreebusyQuery.FBInfo([], [], []),
                 Period.parseText("20080601T000000Z/20080602T000000Z"),
                 Property("ORGANIZER", "mailto:user01 at example.com"),
@@ -230,8 +230,8 @@
             ),
         )
 
-        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)
+        for description, fbinfo, timerange, organizerProp, attendeeProp, event_details, calendar in data:
+            freebusy = FreebusyQuery(organizerProp=organizerProp, attendeeProp=attendeeProp, timerange=timerange, event_details=event_details)
             result = freebusy.buildFreeBusyResult(fbinfo)
             self.assertEqual(normalizeiCalendarText(str(result)), calendar.replace("\n", "\r\n"), msg=description)
 
@@ -313,11 +313,11 @@
 
         calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
         fbinfo = FreebusyQuery.FBInfo([], [], [])
-        matchtotal = 0
         timerange = Period(self.now, self.now_1D)
 
-        freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
-        result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
+        organizer = recipient = yield calendarUserFromCalendarUserAddress("mailto:user01 at example.com", self.transactionUnderTest())
+        freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange)
+        result = (yield freebusy.generateFreeBusyInfo([calendar, ], fbinfo))
         self.assertEqual(result, 0)
         self.assertEqual(len(fbinfo.busy), 0)
         self.assertEqual(len(fbinfo.tentative), 0)
@@ -345,11 +345,11 @@
         yield self._createCalendarObject(data, "user01", "test.ics")
         calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
         fbinfo = FreebusyQuery.FBInfo([], [], [])
-        matchtotal = 0
         timerange = Period(self.now, self.now_1D)
 
-        freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
-        result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
+        organizer = recipient = yield calendarUserFromCalendarUserAddress("mailto:user01 at example.com", self.transactionUnderTest())
+        freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange)
+        result = (yield freebusy.generateFreeBusyInfo([calendar, ], fbinfo))
         self.assertEqual(result, 1)
         self.assertEqual(fbinfo.busy, [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
         self.assertEqual(len(fbinfo.tentative), 0)
@@ -377,19 +377,13 @@
         yield self._createCalendarObject(data, "user01", "test.ics")
         calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
         fbinfo = FreebusyQuery.FBInfo([], [], [])
-        matchtotal = 0
         timerange = Period(self.now, self.now_1D)
         event_details = []
 
-        organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:user01 at example.com", self.transactionUnderTest()))
-
-        freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None, event_details=event_details)
+        organizer = recipient = yield calendarUserFromCalendarUserAddress("mailto:user01 at example.com", self.transactionUnderTest())
+        freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange, event_details=event_details)
         freebusy.same_calendar_user = True
-        result = (yield freebusy.generateFreeBusyInfo(
-            calendar,
-            fbinfo,
-            matchtotal,
-        ))
+        result = yield freebusy.generateFreeBusyInfo([calendar, ], fbinfo)
         self.assertEqual(result, 1)
         self.assertEqual(fbinfo.busy, [Period(self.now_12H, self.now_13H), ])
         self.assertEqual(len(fbinfo.tentative), 0)

Modified: CalendarServer/trunk/txdav/common/datastore/podding/store_api.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/store_api.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/txdav/common/datastore/podding/store_api.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -85,7 +85,7 @@
         txn, request, server = yield self._getRequestForStoreObject("freebusy", calresource, False)
 
         request["organizer"] = organizer
-        request["recipient"] = organizer
+        request["recipient"] = recipient
         request["timerange"] = timerange.getText()
         request["matchtotal"] = matchtotal
         request["excludeuid"] = excludeuid
@@ -107,17 +107,21 @@
         # Operate on the L{CommonHomeChild}
         calresource, _ignore = yield self._getStoreObjectForRequest(txn, request)
 
-        organizer = yield calendarUserFromCalendarUserAddress(request["organizer"], txn)
-        recipient = yield calendarUserFromCalendarUserAddress(request["recipient"], txn)
+        organizer = yield calendarUserFromCalendarUserAddress(request["organizer"], txn) if request["organizer"] else None
+        recipient = yield calendarUserFromCalendarUserAddress(request["recipient"], txn) if request["recipient"] else None
 
         freebusy = FreebusyQuery(
-            organizer, None, recipient, None, None,
-            Period.parseText(request["timerange"]), request["excludeuid"], None, event_details=request["event_details"])
+            organizer=organizer,
+            recipient=recipient,
+            timerange=Period.parseText(request["timerange"]),
+            excludeUID=request["excludeuid"],
+            event_details=request["event_details"],
+        )
         fbinfo = FreebusyQuery.FBInfo([], [], [])
         matchtotal = yield freebusy.generateFreeBusyInfo(
-            calresource,
+            [calresource, ],
             fbinfo,
-            request["matchtotal"],
+            matchtotal=request["matchtotal"],
         )
 
         # Convert L{Period} objects to text for JSON response

Modified: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2015-04-17 21:05:17 UTC (rev 14687)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2015-04-20 18:00:40 UTC (rev 14688)
@@ -931,8 +931,8 @@
         timerange = Period(DateTime.parseText(fbstart), DateTime.parseText(fbend))
         organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:puser01 at example.com", self.theTransactionUnderTest(1)))
 
-        freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None)
-        matchtotal = (yield freebusy.generateFreeBusyInfo(shared, fbinfo, 0))
+        freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange)
+        matchtotal = (yield freebusy.generateFreeBusyInfo([shared, ], fbinfo))
 
         self.assertEqual(matchtotal, 1)
         self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150420/04eb9c2e/attachment-0001.html>


More information about the calendarserver-changes mailing list