[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