[CalendarServer-changes] [11091] CalendarServer/branches/users/cdaboo/store-scheduling

source_changes at macosforge.org source_changes at macosforge.org
Tue Apr 23 07:26:50 PDT 2013


Revision: 11091
          http://trac.calendarserver.org//changeset/11091
Author:   cdaboo at apple.com
Date:     2013-04-23 07:26:50 -0700 (Tue, 23 Apr 2013)
Log Message:
-----------
Checkpoint: freebusy and auto-accept implemented.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/aggregate.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling_store/caldav/resource.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/scheduler.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/scheduler.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_implicit.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/util.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendardirectoryservice.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/freebusy.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_freebusy.py

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/aggregate.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/aggregate.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -86,15 +86,18 @@
             "shortNames" : { },
         }
 
+
     def __repr__(self):
         return "<%s (%s): %r>" % (self.__class__.__name__, self.realmName, self._recordTypes)
 
+
     #
     # Define calendarHomesCollection as a property so we can set it on contained services
     #
     def _getCalendarHomesCollection(self):
         return self._calendarHomesCollection
 
+
     def _setCalendarHomesCollection(self, value):
         for service in self._recordTypes.values():
             service.calendarHomesCollection = value
@@ -108,6 +111,7 @@
     def _getAddressBookHomesCollection(self):
         return self._addressBookHomesCollection
 
+
     def _setAddressBookHomesCollection(self, value):
         for service in self._recordTypes.values():
             service.addressBookHomesCollection = value
@@ -118,6 +122,7 @@
     def recordTypes(self):
         return set(self._recordTypes)
 
+
     def listRecords(self, recordType):
         records = self._query("listRecords", recordType)
         if records is None:
@@ -125,6 +130,7 @@
         else:
             return records
 
+
     def recordWithShortName(self, recordType, shortName):
 
         # FIXME: These temporary records shouldn't be needed when we move
@@ -136,6 +142,7 @@
 
         return self._query("recordWithShortName", recordType, shortName)
 
+
     def recordWithUID(self, uid):
 
         # FIXME: These temporary records shouldn't be needed when we move
@@ -152,9 +159,11 @@
     def recordWithAuthID(self, authID):
         return self._queryAll("recordWithAuthID", authID)
 
+
     def recordWithCalendarUserAddress(self, address):
         return self._queryAll("recordWithCalendarUserAddress", address)
 
+
     def recordWithCachedGroupsAlias(self, recordType, alias):
         """
         @param recordType: the type of the record to look up.
@@ -167,6 +176,7 @@
         service = self.serviceForRecordType(recordType)
         return service.recordWithCachedGroupsAlias(recordType, alias)
 
+
     @inlineCallbacks
     def recordsMatchingFields(self, fields, operand="or", recordType=None):
 
@@ -236,6 +246,7 @@
         except KeyError:
             raise UnknownRecordTypeError(recordType)
 
+
     def _query(self, query, recordType, *args):
         try:
             service = self.serviceForRecordType(recordType)
@@ -247,6 +258,7 @@
             *[a[len(service.recordTypePrefix):] for a in args]
         )
 
+
     def _queryAll(self, query, *args):
         for service in self._recordTypes.values():
             try:
@@ -258,6 +270,7 @@
         else:
             return None
 
+
     def flushCaches(self):
         for service in self._recordTypes.values():
             if hasattr(service, "_initCaches"):
@@ -266,12 +279,13 @@
     userRecordTypes = [DirectoryService.recordType_users]
 
     def requestAvatarId(self, credentials):
-        
+
         if credentials.authnPrincipal:
             return credentials.authnPrincipal.record.service.requestAvatarId(credentials)
-        
+
         raise UnauthorizedLogin("No such user: %s" % (credentials.credentials.username,))
 
+
     def getResourceInfo(self):
         results = []
         for service in self._recordTypes.values():
@@ -295,6 +309,7 @@
             firstName=firstName, lastName=lastName,
             emailAddresses=emailAddresses, uid=uid, password=password, **kwargs)
 
+
     def updateRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
         fullName=None, firstName=None, lastName=None, emailAddresses=set(),
         uid=None, password=None, **kwargs):
@@ -305,10 +320,12 @@
             lastName=lastName, emailAddresses=emailAddresses, uid=uid,
             password=password, **kwargs)
 
+
     def destroyRecord(self, recordType, guid=None):
         service = self.serviceForRecordType(recordType)
         return service.destroyRecord(recordType, guid=guid)
 
+
     def setRealm(self, realmName):
         """
         Set a new realm name for this and nested services
@@ -317,6 +334,20 @@
         for service in self._recordTypes.values():
             service.setRealm(realmName)
 
+
+    def setPrincipalService(self, principalService):
+        """
+        Set the principal service that the directory relies on for doing proxy tests.
+
+        @param principalService: the principal service.
+        @type principalService: L{DirectoryProvisioningResource}
+        """
+        self.principalService = principalService
+        for service in self._recordTypes.values():
+            service.setPrincipalService(principalService)
+
+
+
 class DuplicateRecordTypeError(DirectoryError):
     """
     Duplicate record type.

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/directory/directory.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -562,7 +562,32 @@
         raise NotImplementedError("Subclass must implement createRecords")
 
 
+    def setPrincipalService(self, principalService):
+        """
+        Set the principal service that the directory relies on for doing proxy tests.
 
+        @param principalService: the principal service.
+        @type principalService: L{DirectoryProvisioningResource}
+        """
+        self.principalService = principalService
+
+
+    def isProxyFor(self, test, other):
+        """
+        Test whether one record is a calendar user proxy for the specified record.
+
+        @param test: record to test
+        @type test: L{DirectoryRecord}
+        @param other: record to check against
+        @type other: L{DirectoryRecord}
+
+        @return: C{True} if test is a proxy of other.
+        @rtype: C{bool}
+        """
+        return self.principalService.proxyFor(self, other)
+
+
+
 class GroupMembershipCache(Memcacher, LoggingMixIn):
     """
     Caches group membership information
@@ -1350,7 +1375,8 @@
 
     def autoAcceptFromOrganizer(self, organizer):
         if organizer is not None and self.autoAcceptGroup is not None:
-            organizerRecord = self.service.recordWithCalendarUserAddress(organizer)
+            service = self.service.aggregateService or self.service
+            organizerRecord = service.recordWithCalendarUserAddress(organizer)
             if organizerRecord is not None:
                 if organizerRecord.guid in self.autoAcceptMembers():
                     return True
@@ -1441,7 +1467,20 @@
         return self._cachedAutoAcceptMembers
 
 
+    def isProxyFor(self, other):
+        """
+        Test whether the record is a calendar user proxy for the specified record.
 
+        @param other: record to test
+        @type other: L{DirectoryRecord}
+
+        @return: C{True} if it is a proxy.
+        @rtype: C{bool}
+        """
+        return self.service.proxyFor(self, other)
+
+
+
 class DirectoryError(RuntimeError):
     """
     Generic directory error.

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/ical.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -663,7 +663,7 @@
         """
         result = Component(None, pycalendar=self._pycalendar.duplicate())
         if hasattr(self, "noInstanceIndexing"):
-            result.noInstanceIndexing = True
+            result.noInstanceIndexing = self.noInstanceIndexing
         return result
 
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling_store/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling_store/caldav/resource.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/scheduling_store/caldav/resource.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -14,8 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from txdav.caldav.icalendarstore import InvalidDefaultCalendar
-from twisted.python.failure import Failure
 
 """
 CalDAV scheduling resources.
@@ -42,6 +40,7 @@
 from twext.web2.http import HTTPError
 
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.python.failure import Failure
 
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.caldavxml import caldav_namespace, Opaque, \
@@ -53,6 +52,7 @@
 
 from txdav.base.propertystore.base import PropertyName
 from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
+from txdav.caldav.icalendarstore import InvalidDefaultCalendar
 from txdav.xml import element as davxml
 from txdav.xml.rfc2518 import HRef
 
@@ -374,7 +374,7 @@
 
         calendar = (yield self.loadCalendarFromRequest(request))
         originator = (yield self.loadOriginatorFromRequestDetails(request))
-        recipients = self.loadRecipientsFromCalendarData()
+        recipients = self.loadRecipientsFromCalendarData(calendar)
 
         # This is a local CALDAV scheduling operation.
         scheduler = CalDAVScheduler(self._associatedTransaction, self.parent._newStoreHome.uid())

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/delivery.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -25,8 +25,6 @@
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.method import report_common
-from twistedcaldav.resource import isCalendarCollectionResource
 
 from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser, \
     PartitionedCalendarUser, OtherServerCalendarUser
@@ -38,6 +36,8 @@
 import uuid
 from txdav.base.propertystore.base import PropertyName
 from txdav.caldav.icalendarstore import ComponentUpdateState
+from txdav.caldav.datastore.scheduling.freebusy import processAvailabilityFreeBusy, \
+    generateFreeBusyInfo, buildFreeBusyResult
 
 
 """
@@ -223,18 +223,18 @@
     @inlineCallbacks
     def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):
 
-        # Find the current recipients calendar-free-busy-set
-        fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
+        # 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.
-        has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
-        if has_prop:
-            availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+        availability = recipient.inbox.properties().get(PropertyName(calendarserver_namespace, "calendar-availability"))
+        if availability is not None:
             availability = availability.calendar()
-            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
+            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.
@@ -245,18 +245,9 @@
 
         # Now process free-busy set calendars
         matchtotal = 0
-        for calendarResourceURL in fbset:
-            if not calendarResourceURL.endswith('/'):
-                calendarResourceURL += '/'
-            calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
-            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
-                # We will ignore missing calendars. If the recipient has failed to
-                # properly manage the free busy set that should not prevent us from working.
-                continue
-
-            matchtotal = (yield report_common.generateFreeBusyInfo(
-                self.scheduler.request,
-                calendarResource,
+        for calendar in fbset:
+            matchtotal = (yield generateFreeBusyInfo(
+                calendar,
                 fbinfo,
                 self.scheduler.timeRange,
                 matchtotal,
@@ -266,10 +257,11 @@
                 same_calendar_user=same_calendar_user,
                 servertoserver=remote,
                 event_details=event_details,
+                logItems=self.scheduler.logItems,
             ))
 
         # Build VFREEBUSY iTIP reply for this recipient
-        fbresult = report_common.buildFreeBusyResult(
+        fbresult = buildFreeBusyResult(
             fbinfo,
             self.scheduler.timeRange,
             organizer=organizerProp,

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/scheduler.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/scheduler.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -66,12 +66,12 @@
         self.doingPOST = False
 
 
-    def doSchedulingViaPOST(self):
+    def doSchedulingViaPOST(self, originator, recipients, calendar):
         """
         The Scheduling POST operation on an Outbox.
         """
         self.doingPOST = True
-        return super(CalDAVScheduler, self).doSchedulingViaPOST()
+        return super(CalDAVScheduler, self).doSchedulingViaPOST(originator, recipients, calendar)
 
 
     def checkAuthorization(self):

Added: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -0,0 +1,299 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from pycalendar.datetime import PyCalendarDateTime
+
+from twext.python.clsprop import classproperty
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.trial.unittest import TestCase
+
+from twistedcaldav.ical import Component
+
+from txdav.caldav.datastore.test.util import buildCalendarStore
+from txdav.common.datastore.test.util import CommonCommonTests, populateCalendarsFrom
+from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
+
+def normalizeiCalendarText(data):
+    data = data.replace("\r\n ", "")
+    data = [line for line in data.splitlines() if not (line.startswith("UID") or line.startswith("DTSTAMP"))]
+    return "\r\n".join(data) + "\r\n"
+
+
+
+class SchedulerFreeBusyRequest(CommonCommonTests, TestCase):
+    """
+    Test txdav.caldav.datastore.scheduling.scheduler.doScheduleingViaPOST
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(SchedulerFreeBusyRequest, self).setUp()
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+        yield self.populate()
+
+        self.now = PyCalendarDateTime.getNowUTC()
+        self.now.setHHMMSS(0, 0, 0)
+
+        self.now_12H = self.now.duplicate()
+        self.now_12H.offsetHours(12)
+
+        self.now_13H = self.now.duplicate()
+        self.now_13H.offsetHours(13)
+
+        self.now_1D = self.now.duplicate()
+        self.now_1D.offsetDay(1)
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        return {
+        "user01": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user02": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user03": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+    }
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def _createCalendarObject(self, data, user, name):
+        calendar_collection = (yield self.calendarUnderTest(home=user))
+        yield calendar_collection.createCalendarObjectWithName("test.ics", Component.fromString(data))
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def _listCalendarObjects(self, user, collection_name="calendar_1"):
+        collection = (yield self.calendarUnderTest(name=collection_name, home=user))
+        items = (yield collection.listCalendarObjects())
+        yield self.commit()
+        returnValue(items)
+
+
+    @inlineCallbacks
+    def _getCalendarData(self, user, name=None):
+        if name is None:
+            items = (yield self._listCalendarObjects(user))
+            name = items[0]
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name=name, home=user))
+        calendar = (yield calendar_resource.component())
+        yield self.commit()
+        returnValue(str(calendar).replace("\r\n ", ""))
+
+
+    @inlineCallbacks
+    def _setCalendarData(self, data, user, name=None):
+        if name is None:
+            items = (yield self._listCalendarObjects(user))
+            name = items[0]
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name=name, home=user))
+        yield calendar_resource.setComponent(Component.fromString(data))
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_no_events(self):
+        """
+        Test when the calendar is empty.
+        """
+
+        data_request = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:%s
+DTEND:%s
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""" % (self.now.getText(), self.now_1D.getText(),)
+
+        data_reply = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:%s
+DTEND:%s
+ATTENDEE:mailto:user01 at example.com
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""" % (self.now.getText(), self.now_1D.getText(),)
+
+        scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
+        result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
+        self.assertEqual(len(result.responses), 1)
+        self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
+        self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
+        self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+
+
+    @inlineCallbacks
+    def test_one_event(self):
+        """
+        Test when the calendar is empty.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:%s
+DTEND:%s
+END:VEVENT
+END:VCALENDAR
+""" % (self.now_12H.getText(), self.now_13H.getText(),)
+
+        yield self._createCalendarObject(data, "user01", "test.ics")
+
+        data_request = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:%s
+DTEND:%s
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""" % (self.now.getText(), self.now_1D.getText(),)
+
+        data_reply = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:%s
+DTEND:%s
+ATTENDEE:mailto:user01 at example.com
+FREEBUSY;FBTYPE=BUSY:%s/PT1H
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""" % (self.now.getText(), self.now_1D.getText(), self.now_12H.getText(),)
+
+        scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
+        result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
+        self.assertEqual(len(result.responses), 1)
+        self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
+        self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
+        self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+
+
+    @inlineCallbacks
+    def test_one_event_event_details(self):
+        """
+        Test when the calendar is empty.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:%s
+DTEND:%s
+END:VEVENT
+END:VCALENDAR
+""" % (self.now_12H.getText(), self.now_13H.getText(),)
+
+        yield self._createCalendarObject(data, "user01", "test.ics")
+
+        data_request = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:%s
+DTEND:%s
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+X-CALENDARSERVER-EXTENDED-FREEBUSY:T
+END:VFREEBUSY
+END:VCALENDAR
+""" % (self.now.getText(), self.now_1D.getText(),)
+
+        data_reply = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+DTSTART:%(now_12H)s
+DTEND:%(now_13H)s
+END:VEVENT
+BEGIN:VFREEBUSY
+DTSTART:%(now)s
+DTEND:%(now_1D)s
+ATTENDEE:mailto:user01 at example.com
+FREEBUSY;FBTYPE=BUSY:%(now_12H)s/PT1H
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""" % {
+    "now": self.now.getText(),
+    "now_1D": self.now_1D.getText(),
+    "now_12H": self.now_12H.getText(),
+    "now_13H": self.now_13H.getText(),
+}
+
+        scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
+        result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
+        self.assertEqual(len(result.responses), 1)
+        self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
+        self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
+        self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))

Added: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/freebusy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/freebusy.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/freebusy.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -0,0 +1,631 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.timezone import PyCalendarTimezone
+
+from twext.python.log import Logger
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.config import config
+from twistedcaldav.dateops import compareDateTime, normalizeToUTC, \
+    parseSQLTimestampToPyCalendar, clipPeriod, timeRangesOverlap, \
+    normalizePeriodList
+from twistedcaldav.ical import Component, Property, iCalendarProductID
+from twistedcaldav.instance import InstanceList
+from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.query import calendarqueryfilter
+
+from txdav.caldav.icalendarstore import QueryMaxResources
+from txdav.common.icommondatastore import IndexedSearchException
+
+import uuid
+from txdav.base.propertystore.base import PropertyName
+
+log = Logger()
+
+fbtype_mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
+fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
+
+fbcacher = Memcacher("FBCache", 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 = str(calresource.id()) + "/" + useruid
+        token = (yield calresource.syncToken())
+        entry = (yield fbcacher.get(key))
+
+        if entry:
+
+            # Offset one day at either end to account for floating
+            cached_start = entry.timerange.start + PyCalendarDuration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
+            cached_end = entry.timerange.end - PyCalendarDuration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
+
+            # Verify that the requested time range lies within the cache time range
+            if compareDateTime(timerange.end, cached_end) <= 0 and compareDateTime(timerange.start, cached_start) >= 0:
+
+                # Verify that cached entry is still valid
+                if token == entry.token:
+                    returnValue(entry.fbresults)
+
+        returnValue(None)
+
+
+    @classmethod
+    @inlineCallbacks
+    def makeCacheEntry(cls, calresource, useruid, timerange, fbresults):
+
+        key = str(calresource.id()) + "/" + useruid
+        token = (yield calresource.syncToken())
+        entry = cls(key, token, timerange, fbresults)
+        yield fbcacher.set(key, entry)
+
+
+
+ at inlineCallbacks
+def generateFreeBusyInfo(
+    calresource,
+    fbinfo,
+    timerange,
+    matchtotal,
+    excludeuid=None,
+    organizer=None,
+    organizerPrincipal=None,
+    same_calendar_user=False,
+    servertoserver=False,
+    event_details=None,
+    logItems=None,
+):
+    """
+    Run a free busy report on the specified calendar collection
+    accumulating the free busy info for later processing.
+    @param calresource: the L{Calendar} for a calendar collection.
+    @param fbinfo:      the array of busy periods to update.
+    @param timerange:   the L{TimeRange} for the query.
+    @param matchtotal:  the running total for the number of matches.
+    @param excludeuid:  a C{str} containing a UID value to exclude any
+        components with that UID from contributing to free-busy.
+    @param organizer:   a C{str} containing the value of the ORGANIZER property
+        in the VFREEBUSY request.  This is used in conjunction with the UID
+        value to process exclusions.
+    @param same_calendar_user: a C{bool} indicating whether the calendar user
+        requesting the free-busy information is the same as the calendar user
+        being targeted.
+    @param servertoserver: a C{bool} indicating whether we are doing a local or
+        remote lookup request.
+    @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
+    @param logItems: a C{dict} to store logging info to
+    """
+
+    # First check the privilege on this collection
+    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+    # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy
+
+    # May need organizer principal
+    organizer_record = calresource.directoryService().recordWithCalendarUserAddress(organizer) if organizer else None
+    organizer_uid = organizer_record.uid if organizer_record else ""
+
+    # Free busy is per-user
+    useruid = calresource.ownerHome().uid()
+    user_record = calresource.directoryService().recordWithUID(useruid)
+
+    # Get the timezone property from the collection.
+    tz = calresource.properties().get(PropertyName(caldav_namespace, "calendar-timezone"))
+
+    # Look for possible extended free busy information
+    rich_options = {
+        "organizer": False,
+        "delegate": False,
+        "resource": False,
+    }
+    do_event_details = False
+    if event_details is not None and organizer_record is not None and user_record is not None:
+
+        # Check if organizer is attendee
+        if organizer_uid == useruid:
+            do_event_details = True
+            rich_options["organizer"] = True
+
+        # Check if organizer is a delegate of attendee
+        proxy = (yield organizer_record.isProxyFor(user_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 user_record.getCUType() in ("RESOURCE", "ROOM",):
+            do_event_details = True
+            rich_options["resource"] = True
+
+    # Try cache
+    resources = (yield FBCacheEntry.getCacheEntry(calresource, useruid, timerange)) if config.EnableFreeBusyCache else None
+
+    if resources is None:
+
+        caching = False
+        if config.EnableFreeBusyCache:
+            # Log extended item
+            if logItems is not None:
+                logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1
+
+            # We want to cache a large range of time based on the current date
+            cache_start = normalizeToUTC(PyCalendarDateTime.getToday() + PyCalendarDuration(days=0 - config.FreeBusyCacheDaysBack))
+            cache_end = normalizeToUTC(PyCalendarDateTime.getToday() + PyCalendarDuration(days=config.FreeBusyCacheDaysForward))
+
+            # If the requested time range would fit in our allowed cache range, trigger the cache creation
+            if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime(timerange.end, cache_end) <= 0:
+                cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText())
+                caching = True
+
+        #
+        # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
+        # We then take those results and merge them into one VFREEBUSY component
+        # with appropriate FREEBUSY properties, and return that single item as iCal data.
+        #
+
+        # Create fake filter element to match time-range
+        filter = caldavxml.Filter(
+                      caldavxml.ComponentFilter(
+                          caldavxml.ComponentFilter(
+                              cache_timerange if caching else timerange,
+                              name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+                          ),
+                          name="VCALENDAR",
+                       )
+                  )
+        filter = calendarqueryfilter.Filter(filter)
+        tzinfo = filter.settimezone(tz)
+
+        try:
+            resources = yield calresource._index.indexedSearch(filter, useruid=useruid, fbtype=True)
+            if caching:
+                yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources)
+        except IndexedSearchException:
+            resources = yield calresource._index.bruteForceSearch()
+
+    else:
+        # Log extended item
+        if logItems is not None:
+            logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1
+
+        # Determine appropriate timezone (UTC is the default)
+        tzinfo = tz.gettimezone() if tz is not None else PyCalendarTimezone(utc=True)
+
+    # We care about separate instances for VEVENTs only
+    aggregated_resources = {}
+    for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
+        if transp == 'T' and fbtype != '?':
+            fbtype = 'F'
+        aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
+
+    for key in aggregated_resources.iterkeys():
+
+        name, uid, type, test_organizer = key
+
+        # Short-cut - if an fbtype exists we can use that
+        if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
+
+            matchedResource = False
+
+            # Look at each instance
+            for float, start, end, fbtype in aggregated_resources[key]:
+                # Ignore free time or unknown
+                if fbtype in ('F', '?'):
+                    continue
+
+                # Ignore ones of this UID
+                if excludeuid:
+                    # See if we have a UID match
+                    if (excludeuid == uid):
+                        test_record = calresource.directoryService().recordWithCalendarUserAddress(test_organizer) if test_organizer else None
+                        test_uid = test_record.uid if test_record else ""
+
+                        # Check that ORGANIZER's match (security requirement)
+                        if (organizer is None) or (organizer_uid == test_uid):
+                            continue
+                        # Check for no ORGANIZER and check by same calendar user
+                        elif (test_uid == "") and same_calendar_user:
+                            continue
+
+                # Apply a timezone to any floating times
+                fbstart = parseSQLTimestampToPyCalendar(start)
+                if float == 'Y':
+                    fbstart.setTimezone(tzinfo)
+                else:
+                    fbstart.setTimezone(PyCalendarTimezone(utc=True))
+                fbend = parseSQLTimestampToPyCalendar(end)
+                if float == 'Y':
+                    fbend.setTimezone(tzinfo)
+                else:
+                    fbend.setTimezone(PyCalendarTimezone(utc=True))
+
+                # Clip instance to time range
+                clipped = clipPeriod(PyCalendarPeriod(fbstart, duration=fbend - fbstart), PyCalendarPeriod(timerange.start, timerange.end))
+
+                # Double check for overlap
+                if clipped:
+                    matchedResource = True
+                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)
+
+            if matchedResource:
+                # Check size of results is within limit
+                matchtotal += 1
+                if matchtotal > config.MaxQueryWithDataResults:
+                    raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
+
+                # Add extended details
+                if do_event_details:
+                    child = (yield calresource.calendarObjectWithName(name))
+                    calendar = (yield child.componentForUser())
+                    _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
+
+        else:
+            child = (yield calresource.calendarObjectWithName(name))
+            calendar = (yield child.componentForUser())
+
+            # The calendar may come back as None if the resource is being changed, or was deleted
+            # between our initial index query and getting here. For now we will ignore this error, but in
+            # the longer term we need to implement some form of locking, perhaps.
+            if calendar is None:
+                log.err("Calendar %s is missing from calendar collection %r" % (name, calresource))
+                continue
+
+            # Ignore ones of this UID
+            if excludeuid:
+                # See if we have a UID match
+                if (excludeuid == uid):
+                    test_organizer = calendar.getOrganizer()
+                    test_record = calresource.principalForCalendarUserAddress(test_organizer) if test_organizer else None
+                    test_uid = test_record.principalUID() if test_record else ""
+
+                    # Check that ORGANIZER's match (security requirement)
+                    if (organizer is None) or (organizer_uid == test_uid):
+                        continue
+                    # Check for no ORGANIZER and check by same calendar user
+                    elif (test_organizer is None) and same_calendar_user:
+                        continue
+
+            if filter.match(calendar, None):
+                # Check size of results is within limit
+                matchtotal += 1
+                if matchtotal > config.MaxQueryWithDataResults:
+                    raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
+
+                if calendar.mainType() == "VEVENT":
+                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
+                elif calendar.mainType() == "VFREEBUSY":
+                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
+                elif calendar.mainType() == "VAVAILABILITY":
+                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
+                else:
+                    assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
+
+                # Add extended details
+                if calendar.mainType() == "VEVENT" and do_event_details:
+                    child = (yield calresource.calendarObjectWithName(name))
+                    calendar = (yield child.componentForUser())
+                    _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
+
+    returnValue(matchtotal)
+
+
+
+def _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo):
+    """
+    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{PyCalendarTimezone}
+    """
+
+    # First expand the component
+    expanded = calendar.expand(timerange.start, timerange.end, timezone=tzinfo)
+
+    keep_props = (
+        "UID",
+        "RECURRENCE-ID",
+        "DTSTAMP",
+        "DTSTART",
+        "DTEND",
+        "DURATION",
+    )
+
+    if rich_options["organizer"] or rich_options["delegate"]:
+        keep_props += ("SUMMARY",)
+
+    if rich_options["organizer"] or rich_options["resource"]:
+        keep_props += ("ORGANIZER",)
+
+    # 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() == "VEVENT":
+            for sub in tuple(subcomponent.subcomponents()):
+                subcomponent.removeComponent(sub)
+
+    event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == "VEVENT"])
+
+
+
+def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
+    """
+    Extract free busy data from a VEVENT component.
+    @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
+    @param fbinfo: the tuple used to store the three types of fb data.
+    @param timerange: the time range to restrict free busy data to.
+    @param tzinfo: the L{PyCalendarTimezone} for the timezone to use for floating/all-day events.
+    """
+
+    # Expand out the set of instances for the event with in the required range
+    instances = calendar.expandTimeRanges(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("TRANSP"):
+            # If its TRANSPARENT we always ignore it
+            if instance.component.propertyValue("TRANSP") == "TRANSPARENT":
+                continue
+
+        # Determine status
+        if instance.component.hasProperty("STATUS"):
+            status = instance.component.propertyValue("STATUS")
+        else:
+            status = "CONFIRMED"
+
+        # Ignore cancelled
+        if status == "CANCELLED":
+            continue
+
+        # Clip period for this instance - use duration for period end if that
+        # is what original component used
+        if instance.component.hasProperty("DURATION"):
+            period = PyCalendarPeriod(fbstart, duration=fbend - fbstart)
+        else:
+            period = PyCalendarPeriod(fbstart, fbend)
+        clipped = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))
+
+        # Double check for overlap
+        if clipped:
+            if status == "TENTATIVE":
+                fbinfo[1].append(clipped)
+            else:
+                fbinfo[0].append(clipped)
+
+
+
+def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
+    """
+    Extract FREEBUSY data from a VFREEBUSY component.
+    @param calendar: the L{Component} that is the VCALENDAR containing the VFREEBUSY's.
+    @param fbinfo: the tuple used to store the three types of fb data.
+    @param timerange: the time range to restrict free busy data to.
+    """
+
+    for vfb in [x for x in calendar.subcomponents() if x.name() == "VFREEBUSY"]:
+        # First check any start/end in the actual component
+        start = vfb.getStartDateUTC()
+        end = vfb.getEndDateUTC()
+        if start and end:
+            if not timeRangesOverlap(start, end, timerange.start, timerange.end):
+                continue
+
+        # Now look at each FREEBUSY property
+        for fb in vfb.properties("FREEBUSY"):
+            # Check the type
+            fbtype = fb.parameterValue("FBTYPE", default="BUSY")
+            if fbtype == "FREE":
+                continue
+
+            # Look at each period in the property
+            assert isinstance(fb.value(), list), "FREEBUSY property does not contain a list of values: %r" % (fb,)
+            for period in fb.value():
+                # Clip period for this instance
+                clipped = clipPeriod(period.getValue(), PyCalendarPeriod(timerange.start, timerange.end))
+                if clipped:
+                    fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
+
+
+
+def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
+    """
+    Extract free-busy data from a VAVAILABILITY component.
+    @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
+    @param fbinfo: the tuple used to store the three types of fb data.
+    @param timerange: the time range to restrict free busy data to.
+    """
+
+    for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:
+
+        # Get overall start/end
+        start = vav.getStartDateUTC()
+        if start is None:
+            start = PyCalendarDateTime(1900, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
+        end = vav.getEndDateUTC()
+        if end is None:
+            end = PyCalendarDateTime(2100, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
+        period = PyCalendarPeriod(start, end)
+        overall = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))
+        if overall is None:
+            continue
+
+        # Now get periods for each instance of AVAILABLE sub-components
+        periods = processAvailablePeriods(vav, timerange)
+
+        # Now invert the periods and store in accumulator
+        busyperiods = []
+        last_end = timerange.start
+        for period in periods:
+            if last_end < period.getStart():
+                busyperiods.append(PyCalendarPeriod(last_end, period.getStart()))
+            last_end = period.getEnd()
+        if last_end < timerange.end:
+            busyperiods.append(PyCalendarPeriod(last_end, timerange.end))
+
+        # Add to actual results mapped by busy type
+        fbtype = vav.propertyValue("BUSYTYPE")
+        if fbtype is None:
+            fbtype = "BUSY-UNAVAILABLE"
+
+        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
+
+
+
+def processAvailablePeriods(calendar, timerange):
+    """
+    Extract instance period data from an AVAILABLE component.
+    @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
+    @param timerange: the time range to restrict free busy data to.
+    """
+
+    periods = []
+
+    # First we need to group all AVAILABLE sub-components by UID
+    uidmap = {}
+    for component in calendar.subcomponents():
+        if component.name() == "AVAILABLE":
+            uid = component.propertyValue("UID")
+            uidmap.setdefault(uid, []).append(component)
+
+    # Then we expand each uid set separately
+    for componentSet in uidmap.itervalues():
+        instances = InstanceList(ignoreInvalidInstances=True)
+        instances.expandTimeRanges(componentSet, timerange.end)
+
+        # Now convert instances into period list
+        for key in instances:
+            instance = instances[key]
+            # Ignore any with floating times (which should not happen as the spec requires UTC or local
+            # but we will try and be safe here).
+            start = instance.start
+            if start.floating():
+                continue
+            end = instance.end
+            if end.floating():
+                continue
+
+            # Clip period for this instance - use duration for period end if that
+            # is what original component used
+            if instance.component.hasProperty("DURATION"):
+                period = PyCalendarPeriod(start, duration=end - start)
+            else:
+                period = PyCalendarPeriod(start, end)
+            clipped = clipPeriod(period, PyCalendarPeriod(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):
+    """
+    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.
+    """
+
+    # 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("VCALENDAR")
+    fbcalendar.addProperty(Property("VERSION", "2.0"))
+    fbcalendar.addProperty(Property("PRODID", iCalendarProductID))
+    if method:
+        fbcalendar.addProperty(Property("METHOD", method))
+    fb = Component("VFREEBUSY")
+    fbcalendar.addComponent(fb)
+    if organizer is not None:
+        fb.addProperty(organizer)
+    if attendee is not None:
+        fb.addProperty(attendee)
+    fb.addProperty(Property("DTSTART", timerange.start))
+    fb.addProperty(Property("DTEND", timerange.end))
+    fb.addProperty(Property("DTSTAMP", PyCalendarDateTime.getNowUTC()))
+    if len(fbinfo[0]) != 0:
+        fb.addProperty(Property("FREEBUSY", fbinfo[0], {"FBTYPE": "BUSY"}))
+    if len(fbinfo[1]) != 0:
+        fb.addProperty(Property("FREEBUSY", fbinfo[1], {"FBTYPE": "BUSY-TENTATIVE"}))
+    if len(fbinfo[2]) != 0:
+        fb.addProperty(Property("FREEBUSY", fbinfo[2], {"FBTYPE": "BUSY-UNAVAILABLE"}))
+    if uid is not None:
+        fb.addProperty(Property("UID", uid))
+    else:
+        uid = str(uuid.uuid4())
+        fb.addProperty(Property("UID", uid))
+
+    if event_details:
+        for vevent in event_details:
+            fbcalendar.addComponent(vevent)
+
+    return fbcalendar

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/processing.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/processing.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -33,7 +33,6 @@
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.method import report_common
 
 from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
 from txdav.caldav.datastore.scheduling.itip import iTipProcessing, iTIPRequestStatus
@@ -45,6 +44,8 @@
 from txdav.caldav.icalendarstore import ComponentUpdateState, \
     ComponentRemoveState
 from twext.enterprise.locking import NamedLock
+from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
 
 """
 CalDAV implicit processing.
@@ -550,8 +551,8 @@
 
                 # Let the store know that no time-range info has changed for a refresh (assuming that
                 # no auto-accept changes were made)
-                if hasattr(self.txn, "doing_attendee_refresh") and not send_reply:
-                    new_calendar.noInstanceIndexing = True
+                if hasattr(self.txn, "doing_attendee_refresh"):
+                    new_calendar.noInstanceIndexing = not send_reply
 
                 # Update the attendee's copy of the event
                 log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
@@ -624,7 +625,7 @@
 
                     # Delete the attendee's copy of the event
                     log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
-                    yield self.deleteCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name)
+                    yield self.deleteCalendarResource(self.recipient_calendar_resource)
 
                     # Build the schedule-changes XML element
                     changes = customxml.ScheduleChanges(
@@ -672,50 +673,30 @@
         @return: L{Component} for the new calendar data to write
         """
 
-        # We need to get the UID lock for implicit processing whilst we send the auto-reply
-        # as the Organizer processing will attempt to write out data to other attendees to
-        # refresh them. To prevent a race we need a lock.
-        lock = MemcacheLock(
-            "ImplicitUIDLock",
-            calendar.resourceUID(),
-            timeout=config.Scheduling.Options.UIDLockTimeoutSeconds,
-            expire_time=config.Scheduling.Options.UIDLockExpirySeconds,
-        )
+        # The original transaction is still around but likely committed at this point, so we need a brand new
+        # transaction to do this work.
+        txn = yield self.txn.store().newTransaction("Attendee (%s) auto-reply for UID: %s" % (self.recipient.cuaddr, self.uid,))
 
-        # Note that this lock also protects the request, as this request is
-        # being re-used by potentially multiple transactions and should not be
-        # used concurrency (the locateResource cache needs to be cleared each
-        # time, by inNewTransaction). -glyph
         try:
-            yield lock.acquire()
-        except MemcacheLockTimeoutError:
-            # Just try again to get the lock
-            reactor.callLater(2.0, self.sendAttendeeAutoReply, *(calendar, resource, partstat))
-        else:
-            # inNewTransaction wipes out the remembered resource<-> URL mappings in the
-            # request object but we need to be able to map the actual reply resource to its
-            # URL when doing auto-processing, so we have to sneak that mapping back in here.
-            txn = yield resource.inNewTransaction(self.request, label="Send Attendee auto-reply")
+            # We need to get the UID lock for implicit processing whilst we send the auto-reply
+            # as the Organizer processing will attempt to write out data to other attendees to
+            # refresh them. To prevent a race we need a lock.
+            yield NamedLock.acquire(txn, "ImplicitUIDLock:%s" % (hashlib.md5(calendar.resourceUID()).hexdigest(),))
 
-            try:
-                self.request._rememberResource(resource, resource._url)
-                # Send out a reply
-                log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - auto-reply: %s" % (self.recipient.cuaddr, self.uid, partstat))
-                from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
-                scheduler = ImplicitScheduler()
-                yield scheduler.sendAttendeeReply(txn, resource, calendar, self.recipient)
-            except Exception, e:
-                log.debug("ImplicitProcessing - auto-reply exception UID: '%s', %s" % (self.uid, str(e)))
-                yield txn.abort()
-            except:
-                log.debug("ImplicitProcessing - auto-reply bare exception UID: '%s'" % (self.uid,))
-                yield txn.abort()
-            else:
-                yield txn.commit()
+            # Send out a reply
+            log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - auto-reply: %s" % (self.recipient.cuaddr, self.uid, partstat))
+            from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
+            scheduler = ImplicitScheduler()
+            yield scheduler.sendAttendeeReply(txn, resource, calendar, self.recipient)
+        except Exception, e:
+            log.debug("ImplicitProcessing - auto-reply exception UID: '%s', %s" % (self.uid, str(e)))
+            yield txn.abort()
+        except:
+            log.debug("ImplicitProcessing - auto-reply bare exception UID: '%s'" % (self.uid,))
+            yield txn.abort()
+        else:
+            yield txn.commit()
         finally:
-            # This correctly gets called only after commit or abort is done
-            yield lock.clean()
-
             # Track outstanding auto-reply processing
             if hasattr(self.txn, "auto_reply_processing_count"):
                 self.txn.auto_reply_processing_count -= 1
@@ -748,7 +729,7 @@
 
         log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s" % (self.recipient.cuaddr, self.uid, automode,))
 
-        cuas = self.recipient.principal.calendarUserAddresses()
+        cuas = self.recipient.principal.calendarUserAddresses
 
         # First expand current one to get instances (only go 1 year into the future)
         default_future_expansion_duration = PyCalendarDuration(days=config.Scheduling.Options.AutoSchedule.FutureFreeBusyDays)
@@ -783,16 +764,15 @@
         uid = comp.propertyValue("UID")
 
         # Now compare each instance time-range with the index and see if there is an overlap
-        calendars = (yield self._getCalendarsToMatch())
+        fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
+        fbset = [fbcalendar for fbcalendar in fbset if fbcalendar.isUsedForFreeBusy()]
 
-        for calURL in calendars:
-            testcal = (yield self.request.locateResource(calURL))
+        for testcal in fbset:
 
             # Get the timezone property from the collection, and store in the query filter
             # for use during the query itself.
-            has_prop = (yield testcal.hasProperty((caldav_namespace, "calendar-timezone"), self.request))
-            if has_prop:
-                tz = (yield testcal.readProperty((caldav_namespace, "calendar-timezone"), self.request))
+            tz = testcal.properties().get(PropertyName(caldav_namespace, "calendar-timezone"))
+            if tz is not None:
                 tzinfo = tz.calendar().gettimezone()
             else:
                 tzinfo = PyCalendarTimezone(utc=True)
@@ -820,7 +800,7 @@
                             end=str(makeTimedUTC(instance.end)),
                         )
 
-                        yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
+                        yield generateFreeBusyInfo(testcal, fbinfo, tr, 0, uid, servertoserver=True)
 
                         # If any fbinfo entries exist we have an overlap
                         if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
@@ -923,13 +903,6 @@
         returnValue((made_changes, store_inbox, partstat,))
 
 
-    def _getCalendarsToMatch(self):
-        # Determine the set of calendar URIs for a principal need to be searched.
-
-        # Find the current recipients calendar-free-busy-set
-        return self.recipient.principal.calendarFreeBusyURIs(self.request)
-
-
     @inlineCallbacks
     def writeCalendarResource(self, collection, resource, calendar):
         """
@@ -1030,8 +1003,8 @@
             raise ImplicitProcessorException("5.1;Service unavailable")
 
         # Locate the originator's copy of the event
-        calendar_resource, _ignore_name, _ignore_collection, _ignore_uri = (yield getCalendarObjectForRecord(self.txn, self.originator.principal, self.uid))
-        if not calendar_resource:
+        calendar_resource = (yield getCalendarObjectForRecord(self.txn, self.originator.principal, self.uid))
+        if calendar_resource is None:
             raise ImplicitProcessorException("5.1;Service unavailable")
         originator_calendar = (yield calendar_resource.componentForUser(self.originator.principal.uid))
 
@@ -1039,13 +1012,13 @@
         originator_calendar.attendeesView((self.recipient.cuaddr,))
 
         # Locate the attendee's copy of the event if it exists.
-        recipient_resource, recipient_resource_name, recipient_collection, recipient_collection_uri = (yield getCalendarObjectForRecord(self.txn, self.recipient.principal, self.uid))
+        recipient_resource = (yield getCalendarObjectForRecord(self.txn, self.recipient.principal, self.uid))
 
         # We only need to fix data that already exists
-        if recipient_resource:
+        if recipient_resource is not None:
             if originator_calendar.mainType() != None:
-                yield self.writeCalendarResource(recipient_collection_uri, recipient_collection, recipient_resource_name, originator_calendar)
+                yield self.writeCalendarResource(recipient_resource, originator_calendar)
             else:
-                yield self.deleteCalendarResource(recipient_collection_uri, recipient_collection, recipient_resource_name)
+                yield self.deleteCalendarResource(recipient_resource)
 
         returnValue(True)

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/scheduler.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/scheduler.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -170,7 +170,7 @@
         # of data for all events with the same UID. So detect this and use a lock
         if calendar.resourceType() != "VFREEBUSY":
             uid = calendar.resourceUID()
-            yield NamedLock.acquire(self._txn, "ImplicitUIDLock:%s" % (hashlib.md5(uid).hexdigest(),))
+            yield NamedLock.acquire(self.txn, "ImplicitUIDLock:%s" % (hashlib.md5(uid).hexdigest(),))
 
         result = (yield self.doSchedulingDirectly("POST", originator, recipients, calendar))
         returnValue(result)
@@ -234,11 +234,10 @@
 
     @inlineCallbacks
     def loadFromRequestData(self):
-        yield self.loadOriginatorFromRequestDetails()
+        self.loadOriginatorFromRequestDetails()
         self.loadRecipientsFromCalendarData()
 
 
-    @inlineCallbacks
     def loadOriginatorFromRequestDetails(self):
         # Get the originator who is the authenticated user
         originatorPrincipal = self.txn.directoryService().recordWithUID(self.originator_uid)

Added: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_freebusy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_freebusy.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_freebusy.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -0,0 +1,413 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.period import PyCalendarPeriod
+
+from twext.python.clsprop import classproperty
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.trial.unittest import TestCase
+
+from twistedcaldav import caldavxml
+from twistedcaldav.ical import Component, Property
+
+from txdav.caldav.datastore.scheduling.freebusy import buildFreeBusyResult, \
+    generateFreeBusyInfo
+from txdav.caldav.datastore.test.util import buildCalendarStore
+from txdav.common.datastore.test.util import CommonCommonTests, populateCalendarsFrom
+
+def normalizeiCalendarText(data):
+    data = data.replace("\r\n ", "")
+    data = [line for line in data.splitlines() if not (line.startswith("UID") or line.startswith("DTSTAMP"))]
+    return "\r\n".join(data) + "\r\n"
+
+
+
+class BuildFreeBusyResult (TestCase):
+    """
+    Test txdav.caldav.datastore.scheduling.freebusy.buildFreeBusyResult
+    """
+
+    def test_simple(self):
+
+        data = (
+            (
+                "#1.1 No busy time",
+                [
+                    [],
+                    [],
+                    [],
+                ],
+                "20080601T000000Z",
+                "20080602T000000Z",
+                None,
+                None,
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:20080601T000000Z
+DTEND:20080602T000000Z
+END:VFREEBUSY
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.2 No busy time with organizer & attendee",
+                [
+                    [],
+                    [],
+                    [],
+                ],
+                "20080601T000000Z",
+                "20080602T000000Z",
+                Property("ORGANIZER", "mailto:user01 at example.com"),
+                Property("ATTENDEE", "mailto:user02 at example.com"),
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:20080601T000000Z
+DTEND:20080602T000000Z
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.3 With single busy time",
+                [
+                    [PyCalendarPeriod.parseText("20080601T120000Z/20080601T130000Z"), ],
+                    [],
+                    [],
+                ],
+                "20080601T000000Z",
+                "20080602T000000Z",
+                Property("ORGANIZER", "mailto:user01 at example.com"),
+                Property("ATTENDEE", "mailto:user02 at example.com"),
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:20080601T000000Z
+DTEND:20080602T000000Z
+ATTENDEE:mailto:user02 at example.com
+FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.4 With multiple busy time",
+                [
+                    [
+                        PyCalendarPeriod.parseText("20080601T120000Z/20080601T130000Z"),
+                        PyCalendarPeriod.parseText("20080601T140000Z/20080601T150000Z"),
+                    ],
+                    [],
+                    [],
+                ],
+                "20080601T000000Z",
+                "20080602T000000Z",
+                Property("ORGANIZER", "mailto:user01 at example.com"),
+                Property("ATTENDEE", "mailto:user02 at example.com"),
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:20080601T000000Z
+DTEND:20080602T000000Z
+ATTENDEE:mailto:user02 at example.com
+FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z,20080601T140000Z/20080601T150000Z
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.5 With multiple busy time, some overlap",
+                [
+                    [
+                        PyCalendarPeriod.parseText("20080601T120000Z/20080601T130000Z"),
+                        PyCalendarPeriod.parseText("20080601T123000Z/20080601T133000Z"),
+                        PyCalendarPeriod.parseText("20080601T140000Z/20080601T150000Z"),
+                        PyCalendarPeriod.parseText("20080601T150000Z/20080601T160000Z"),
+                    ],
+                    [],
+                    [],
+                ],
+                "20080601T000000Z",
+                "20080602T000000Z",
+                Property("ORGANIZER", "mailto:user01 at example.com"),
+                Property("ATTENDEE", "mailto:user02 at example.com"),
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:20080601T000000Z
+DTEND:20080602T000000Z
+ATTENDEE:mailto:user02 at example.com
+FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T133000Z,20080601T140000Z/20080601T160000Z
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.6 With all busy time types",
+                [
+                    [
+                        PyCalendarPeriod.parseText("20080601T120000Z/20080601T130000Z"),
+                        PyCalendarPeriod.parseText("20080601T140000Z/20080601T150000Z"),
+                    ],
+                    [
+                        PyCalendarPeriod.parseText("20080601T140000Z/20080601T150000Z"),
+                    ],
+                    [
+                        PyCalendarPeriod.parseText("20080601T160000Z/20080601T170000Z"),
+                    ],
+                ],
+                "20080601T000000Z",
+                "20080602T000000Z",
+                Property("ORGANIZER", "mailto:user01 at example.com"),
+                Property("ATTENDEE", "mailto:user02 at example.com"),
+                None,
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+DTSTART:20080601T000000Z
+DTEND:20080602T000000Z
+ATTENDEE:mailto:user02 at example.com
+FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z,20080601T140000Z/20080601T150000Z
+FREEBUSY;FBTYPE=BUSY-TENTATIVE:20080601T140000Z/20080601T150000Z
+FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20080601T160000Z/20080601T170000Z
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""",
+            ),
+            (
+                "#1.7 With single busy time and event details",
+                [
+                    [PyCalendarPeriod.parseText("20080601T120000Z/20080601T130000Z"), ],
+                    [],
+                    [],
+                ],
+                "20080601T000000Z",
+                "20080602T000000Z",
+                Property("ORGANIZER", "mailto:user01 at example.com"),
+                Property("ATTENDEE", "mailto:user02 at example.com"),
+                [
+                    tuple(Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+END:VCALENDAR
+""").subcomponents())[0],
+                ],
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+BEGIN:VFREEBUSY
+DTSTART:20080601T000000Z
+DTEND:20080602T000000Z
+ATTENDEE:mailto:user02 at example.com
+FREEBUSY;FBTYPE=BUSY:20080601T120000Z/20080601T130000Z
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
+""",
+            ),
+        )
+
+        for description, fbinfo, dtstart, dtend, organizer, attendee, event_details, calendar in data:
+            timerange = caldavxml.TimeRange(start=dtstart, end=dtend)
+            result = buildFreeBusyResult(fbinfo, timerange, organizer=organizer, attendee=attendee, event_details=event_details)
+            self.assertEqual(normalizeiCalendarText(str(result)), calendar.replace("\n", "\r\n"), msg=description)
+
+
+
+class GenerateFreeBusyInfo(CommonCommonTests, TestCase):
+    """
+    Test txdav.caldav.datastore.scheduling.freebusy.generateFreeBusyInfo
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(GenerateFreeBusyInfo, self).setUp()
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+        yield self.populate()
+
+        self.now = PyCalendarDateTime.getNowUTC()
+        self.now.setHHMMSS(0, 0, 0)
+
+        self.now_12H = self.now.duplicate()
+        self.now_12H.offsetHours(12)
+
+        self.now_13H = self.now.duplicate()
+        self.now_13H.offsetHours(13)
+
+        self.now_1D = self.now.duplicate()
+        self.now_1D.offsetDay(1)
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        return {
+        "user01": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user02": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+        "user03": {
+            "calendar_1": {
+            },
+            "inbox": {
+            },
+        },
+    }
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def _createCalendarObject(self, data, user, name):
+        calendar_collection = (yield self.calendarUnderTest(home=user))
+        yield calendar_collection.createCalendarObjectWithName("test.ics", Component.fromString(data))
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_no_events(self):
+        """
+        Test when the calendar is empty.
+        """
+
+        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
+        fbinfo = [[], [], [], ]
+        matchtotal = 0
+        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
+        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
+        self.assertEqual(result, 0)
+        self.assertEqual(len(fbinfo[0]), 0)
+        self.assertEqual(len(fbinfo[1]), 0)
+        self.assertEqual(len(fbinfo[2]), 0)
+
+
+    @inlineCallbacks
+    def test_one_event(self):
+        """
+        Test when the calendar is empty.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:%s
+DTEND:%s
+END:VEVENT
+END:VCALENDAR
+""" % (self.now_12H.getText(), self.now_13H.getText(),)
+
+        yield self._createCalendarObject(data, "user01", "test.ics")
+        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
+        fbinfo = [[], [], [], ]
+        matchtotal = 0
+        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
+        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
+        self.assertEqual(result, 1)
+        self.assertEqual(fbinfo[0], [PyCalendarPeriod.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
+        self.assertEqual(len(fbinfo[1]), 0)
+        self.assertEqual(len(fbinfo[2]), 0)
+
+
+    @inlineCallbacks
+    def test_one_event_event_details(self):
+        """
+        Test when the calendar is empty.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:1234-5678
+DTSTAMP:20080601T000000Z
+DTSTART:%s
+DTEND:%s
+END:VEVENT
+END:VCALENDAR
+""" % (self.now_12H.getText(), self.now_13H.getText(),)
+
+        yield self._createCalendarObject(data, "user01", "test.ics")
+        calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
+        fbinfo = [[], [], [], ]
+        matchtotal = 0
+        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
+        event_details = []
+        result = (yield generateFreeBusyInfo(
+            calendar,
+            fbinfo,
+            timerange,
+            matchtotal,
+            organizer="mailto:user01 at example.com",
+            event_details=event_details
+        ))
+        self.assertEqual(result, 1)
+        self.assertEqual(fbinfo[0], [PyCalendarPeriod.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
+        self.assertEqual(len(fbinfo[1]), 0)
+        self.assertEqual(len(fbinfo[2]), 0)
+        self.assertEqual(len(event_details), 1)
+        self.assertEqual(str(event_details[0]), str(tuple(Component.fromString(data).subcomponents())[0]))

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_implicit.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/scheduling/test/test_implicit.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -23,9 +23,9 @@
 
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 from twisted.trial.unittest import TestCase
+from twistedcaldav.config import config
 
 from twistedcaldav.ical import Component
-import twistedcaldav.test.util
 
 from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
 from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
@@ -36,7 +36,6 @@
 
 import hashlib
 import sys
-from twistedcaldav.config import config
 
 class FakeScheduler(object):
     """
@@ -69,7 +68,7 @@
 
 
 
-class Implicit (twistedcaldav.test.util.TestCase):
+class Implicit(TestCase):
     """
     iCalendar support tests
     """
@@ -872,9 +871,9 @@
 
 
 
-class ImplicitRequests (CommonCommonTests, TestCase):
+class ImplicitRequests(CommonCommonTests, TestCase):
     """
-    Test twistedcaldav.scheduyling.implicit with a Request object.
+    Test twistedcaldav.scheduling.implicit with a Request object.
     """
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -1102,6 +1102,17 @@
         return self.name() == "inbox"
 
 
+    def isUsedForFreeBusy(self):
+        """
+        Indicates whether the contents of this calendar contributes to free busy.
+
+        @return: C{True} if it does, C{False} otherwise
+        @rtype: C{bool}
+        """
+        opaque = self.properties().get(PropertyName(*ScheduleCalendarTransp.qname()), ScheduleCalendarTransp(Opaque())) == ScheduleCalendarTransp(Opaque())
+        return opaque and not self.isInbox()
+
+
     def initPropertyStore(self, props):
         # Setup peruser special properties
         props.setSpecialProperties(
@@ -1573,7 +1584,7 @@
                     authz = self.directoryService().recordWithUID(self._txn._authz_uid)
                     prop = Property("X-CALENDARSERVER-MODIFIED-BY", authz.canonicalCalendarUserAddress())
                     prop.setParameter("CN", authz.displayName())
-                    for candidate in authz.calendarUserAddresses():
+                    for candidate in authz.calendarUserAddresses:
                         if candidate.startswith("mailto:"):
                             prop.setParameter("EMAIL", candidate[7:])
                             break
@@ -2066,7 +2077,7 @@
         # In some cases there is no need to remove/rebuild the instance index because we know no time or
         # freebusy related properties have changed (e.g. an attendee reply and refresh). In those cases
         # the component will have a special attribute present to let us know to suppress the instance indexing.
-        instanceIndexingRequired = not hasattr(component, "noInstanceIndexing") or inserting or reCreate
+        instanceIndexingRequired = not getattr(component, "noInstanceIndexing", False) or inserting or reCreate
 
         if instanceIndexingRequired:
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/util.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/util.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -125,7 +125,11 @@
         return "automatic"
 
 
+    def isProxyFor(self, other):
+        return False
 
+
+
 def buildDirectory(homes=None):
 
     directory = TestCalendarStoreDirectoryService()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendardirectoryservice.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendardirectoryservice.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendardirectoryservice.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -122,3 +122,14 @@
         @return: C{True} if automatically scheduled.
         @rtype: C{bool}
         """
+
+    def isProxyFor(other): #@NoSelf
+        """
+        Test whether the record is a calendar user proxy for the specified record.
+
+        @param other: record to test
+        @type other: L{IDirectoryRecord}
+
+        @return: C{True} if it is a proxy.
+        @rtype: C{bool}
+        """

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-23 01:05:05 UTC (rev 11090)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-23 14:26:50 UTC (rev 11091)
@@ -272,6 +272,16 @@
 
 
 
+class QueryMaxResources(CommonStoreError):
+    """
+    A query-based request for resources returned more resources than the server is willing to deal with in one go.
+    """
+
+    def __init__(self, limit, actual):
+        super(QueryMaxResources, self).__init__("Query result count limit (%s) exceeded: %s" % (limit, actual,))
+
+
+
 class ICalendarTransaction(ICommonTransaction):
     """
     Transaction functionality required to be implemented by calendar stores.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130423/b9dc5f7a/attachment-0001.html>


More information about the calendarserver-changes mailing list