<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14683] CalendarServer/trunk</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14683">14683</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-04-17 11:11:45 -0700 (Fri, 17 Apr 2015)</dd>
</dl>
<h3>Log Message</h3>
<pre>Refactor freebusy queries and remove a bunch of duplicate code, in preparation for trying to do some optimizations.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunktwistedcaldavfreebusyurlpy">CalendarServer/trunk/twistedcaldav/freebusyurl.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethodreport_commonpy">CalendarServer/trunk/twistedcaldav/method/report_common.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethodreport_freebusypy">CalendarServer/trunk/twistedcaldav/method/report_freebusy.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingcaldavdeliverypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingfreebusypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_freebusypy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastorepoddingstore_apipy">CalendarServer/trunk/txdav/common/datastore/podding/store_api.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastorepoddingtesttest_conduitpy">CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunktwistedcaldavfreebusyurlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/freebusyurl.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/freebusyurl.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/freebusyurl.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -26,7 +26,6 @@
</span><span class="cx">
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from txweb2 import responsecode
</span><del>-from txdav.xml import element as davxml
</del><span class="cx"> from txweb2.dav.http import ErrorResponse
</span><span class="cx"> from txweb2.dav.util import joinURL
</span><span class="cx"> from txweb2.http import HTTPError
</span><span class="lines">@@ -36,19 +35,19 @@
</span><span class="cx"> from txweb2.stream import MemoryStream
</span><span class="cx">
</span><span class="cx"> from twistedcaldav import caldavxml
</span><del>-from twistedcaldav.caldavxml import TimeRange
</del><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.customxml import calendarserver_namespace
</span><span class="cx"> from twistedcaldav.ical import Property
</span><span class="cx"> from twistedcaldav.resource import CalDAVResource, ReadOnlyNoCopyResourceMixIn
</span><span class="cx"> from twistedcaldav.scheduling_store.caldav.resource import deliverSchedulePrivilegeSet
</span><span class="cx">
</span><del>-from txdav.caldav.datastore.scheduling.caldav.delivery import ScheduleViaCalDAV
</del><span class="cx"> from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
</span><del>-from txdav.caldav.datastore.scheduling.scheduler import Scheduler
</del><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
+from txdav.xml import element as davxml
</ins><span class="cx">
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="cx"> from pycalendar.duration import Duration
</span><ins>+from pycalendar.period import Period
</ins><span class="cx">
</span><span class="cx"> log = Logger()
</span><span class="cx">
</span><span class="lines">@@ -266,26 +265,16 @@
</span><span class="cx"> if inbox is None:
</span><span class="cx"> raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))
</span><span class="cx">
</span><del>- scheduler = Scheduler(request, self)
- scheduler.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
- scheduler.timeRange.start = self.start
- scheduler.timeRange.end = self.end
</del><ins>+ organizer = recipient = LocalCalendarUser(cuaddr, principal.record)
+ recipient.inbox = inbox._newStoreObject
+ attendeeProp = Property("ATTENDEE", recipient.cuaddr)
+ timerange = Period(self.start, self.end)
</ins><span class="cx">
</span><del>- scheduler.organizer = LocalCalendarUser(cuaddr, principal.record)
- scheduler.organizer.inbox = inbox._newStoreObject
</del><ins>+ fbresult = (yield FreebusyQuery(
+ organizer, None, recipient, attendeeProp, None,
+ timerange, None, None,
+ ).generateAttendeeFreeBusyResponse())
</ins><span class="cx">
</span><del>- attendeeProp = Property("ATTENDEE", scheduler.organizer.cuaddr)
-
- requestor = ScheduleViaCalDAV(scheduler, (), [], True)
- fbresult = (yield requestor.generateAttendeeFreeBusyResponse(
- scheduler.organizer,
- None,
- None,
- None,
- attendeeProp,
- True,
- ))
-
</del><span class="cx"> response = Response()
</span><span class="cx"> response.stream = MemoryStream(str(fbresult))
</span><span class="cx"> response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethodreport_commonpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/method/report_common.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -29,55 +29,33 @@
</span><span class="cx"> "buildFreeBusyResult",
</span><span class="cx"> ]
</span><span class="cx">
</span><del>-import time
-
-try:
- from hashlib import md5
-except ImportError:
- from md5 import new as md5
-
</del><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from twisted.python.failure import Failure
</span><span class="cx">
</span><span class="cx"> from txweb2 import responsecode
</span><span class="cx"> from txweb2.dav.http import statusForFailure
</span><span class="cx"> from txweb2.dav.method.propfind import propertyName
</span><del>-from txweb2.dav.method.report import NumberOfMatchesWithinLimits
-from txweb2.dav.method.report import max_number_of_matches
</del><span class="cx"> from txweb2.dav.resource import AccessDeniedError
</span><del>-from txweb2.http import HTTPError, StatusResponse
</del><ins>+from txweb2.http import HTTPError
</ins><span class="cx">
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx">
</span><span class="cx"> from twistedcaldav import caldavxml
</span><span class="cx"> from twistedcaldav import carddavxml
</span><del>-from twistedcaldav.caldavxml import CalendarData, CalendarTimeZone, TimeRange
</del><ins>+from twistedcaldav.caldavxml import CalendarData
</ins><span class="cx"> from twistedcaldav.carddavxml import AddressData
</span><del>-from twistedcaldav.config import config
</del><span class="cx"> from twistedcaldav.datafilters.calendardata import CalendarDataFilter
</span><span class="cx"> from twistedcaldav.datafilters.hiddeninstance import HiddenInstanceFilter
</span><span class="cx"> from twistedcaldav.datafilters.privateevents import PrivateEventFilter
</span><span class="cx"> from twistedcaldav.datafilters.addressdata import AddressDataFilter
</span><del>-from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap, \
- compareDateTime, normalizeToUTC, parseSQLTimestampToPyCalendar
-from twistedcaldav.ical import Component, Property, iCalendarProductID
-from twistedcaldav.instance import InstanceList
-from twistedcaldav.memcacher import Memcacher
</del><span class="cx">
</span><del>-from txdav.caldav.datastore.query.filter import Filter
-from txdav.common.icommondatastore import IndexedSearchException
</del><span class="cx"> from txdav.xml import element
</span><span class="cx">
</span><del>-from pycalendar.duration import Duration
-from pycalendar.datetime import DateTime
-from pycalendar.timezone import Timezone
-from pycalendar.period import Period
-
</del><span class="cx"> log = Logger()
</span><span class="cx">
</span><span class="cx"> COLLECTION_TYPE_REGULAR = "collection"
</span><span class="cx"> COLLECTION_TYPE_CALENDAR = "calendar"
</span><del>-COLLECTION_TYPE_ADDRESSBOOK = "adressbook"
</del><ins>+COLLECTION_TYPE_ADDRESSBOOK = "addressbook"
</ins><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def applyToCalendarCollections(resource, request, request_uri, depth, apply, privileges):
</span><span class="lines">@@ -422,607 +400,3 @@
</span><span class="cx"> properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
</span><span class="cx">
</span><span class="cx"> returnValue(properties_by_status)
</span><del>-
-fbtype_mapper = {"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 = calresource.resourceID() + "/" + useruid
- token = (yield calresource.getInternalSyncToken())
- entry = (yield fbcacher.get(key))
-
- if entry:
-
- # Offset one day at either end to account for floating
- cached_start = entry.timerange.start + Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
- cached_end = entry.timerange.end - Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
-
- # Verify that the requested timerange lies within the cache timerange
- if compareDateTime(timerange.end, cached_end) <= 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 = calresource.resourceID() + "/" + useruid
- token = (yield calresource.getInternalSyncToken())
- entry = cls(key, token, timerange, fbresults)
- yield fbcacher.set(key, entry)
-
-
-
-@inlineCallbacks
-def generateFreeBusyInfo(
- request,
- calresource,
- fbinfo,
- timerange,
- matchtotal,
- excludeuid=None,
- organizer=None,
- organizerPrincipal=None,
- same_calendar_user=False,
- servertoserver=False,
- event_details=None,
-):
- """
- Run a free busy report on the specified calendar collection
- accumulating the free busy info for later processing.
- @param request: the L{IRequest} for the current request.
- @param calresource: the L{CalDAVResource} for a calendar collection.
- @param fbinfo: the array of busy periods to update.
- @param timerange: the L{TimeRange} for the query.
- @param matchtotal: the running total for the number of matches.
- @param excludeuid: a C{str} containing a UID value to exclude any
- components with that UID from contributing to free-busy.
- @param organizer: a C{str} containing the value of the ORGANIZER property
- in the VFREEBUSY request. This is used in conjunction with the UID
- value to process exclusions.
- @param same_calendar_user: a C{bool} indicating whether the calendar user
- requesting the free-busy information is the same as the calendar user
- being targeted.
- @param servertoserver: a C{bool} indicating whether we are doing a local or
- remote lookup request.
- @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
- """
-
- # First check the privilege on this collection
- # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
- if not servertoserver:
- try:
- yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), principal=organizerPrincipal)
- except AccessDeniedError:
- returnValue(matchtotal)
-
- # May need organizer principal
- organizer_principal = (yield calresource.principalForCalendarUserAddress(organizer)) if organizer else None
- organizer_uid = organizer_principal.principalUID() if organizer_principal else ""
-
- # Free busy is per-user
- userPrincipal = (yield calresource.resourceOwnerPrincipal(request))
- if userPrincipal:
- useruid = userPrincipal.principalUID()
- else:
- useruid = ""
-
- # Get the timezone property from the collection.
- has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
- if has_prop:
- tz = (yield calresource.readProperty(CalendarTimeZone(), request))
- else:
- tz = None
-
- # Look for possible extended free busy information
- rich_options = {
- "organizer": False,
- "delegate": False,
- "resource": False,
- }
- do_event_details = False
- if event_details is not None and organizer_principal is not None and userPrincipal is not None:
-
- # Check if organizer is attendee
- if organizer_principal == userPrincipal:
- do_event_details = True
- rich_options["organizer"] = True
-
- # Check if organizer is a delegate of attendee
- proxy = (yield organizer_principal.isProxyFor(userPrincipal))
- if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
- do_event_details = True
- rich_options["delegate"] = True
-
- # Check if attendee is room or resource
- if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.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 not hasattr(request, "extendedLogItems"):
- request.extendedLogItems = {}
- request.extendedLogItems["fb-uncached"] = request.extendedLogItems.get("fb-uncached", 0) + 1
-
- # We want to cache a large range of time based on the current date
- cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
- cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
-
- # If the requested timerange 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 = Filter(filter)
- tzinfo = filter.settimezone(tz)
-
- try:
- resources = yield calresource.search(filter, useruid=useruid, fbtype=True)
- if caching:
- yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources)
- except IndexedSearchException:
- raise HTTPError(StatusResponse(
- responsecode.INTERNAL_SERVER_ERROR,
- "Failed freebusy query"
- ))
-
- else:
- # Log extended item
- if not hasattr(request, "extendedLogItems"):
- request.extendedLogItems = {}
- request.extendedLogItems["fb-cached"] = request.extendedLogItems.get("fb-cached", 0) + 1
-
- # Determine appropriate timezone (UTC is the default)
- tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
-
- # We care about separate instances for VEVENTs only
- aggregated_resources = {}
- for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
- 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_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None
- test_uid = test_principal.principalUID() if test_principal 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(Timezone(utc=True))
- fbend = parseSQLTimestampToPyCalendar(end)
- if float == 'Y':
- fbend.setTimezone(tzinfo)
- else:
- fbend.setTimezone(Timezone(utc=True))
-
- # Clip instance to time range
- clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end))
-
- # Double check for overlap
- if clipped:
- matchedResource = True
- fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)
-
- if matchedResource:
- # Check size of results is within limit
- matchtotal += 1
- if matchtotal > max_number_of_matches:
- raise NumberOfMatchesWithinLimits(max_number_of_matches)
-
- # Add extended details
- if do_event_details:
- child = (yield request.locateChildResource(calresource, name))
- calendar = (yield child.componentForUser())
- _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
-
- else:
- child = (yield request.locateChildResource(calresource, name))
- calendar = (yield child.componentForUser())
-
- # The calendar may come back as None if the resource is being changed, or was deleted
- # between our initial index query and getting here. For now we will ignore this error, but in
- # the longer term we need to implement some form of locking, perhaps.
- if calendar is None:
- log.error("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_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None
- test_uid = test_principal.principalUID() if test_principal 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 > max_number_of_matches:
- raise NumberOfMatchesWithinLimits(max_number_of_matches)
-
- 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 request.locateChildResource(calresource, name))
- calendar = (yield child.componentForUser())
- _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
-
- returnValue(matchtotal)
-
-
-
-def _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo):
- """
- Expand events within the specified time range and limit the set of properties to those allowed for
- delegate extended free busy.
-
- @param calendar: the calendar object to expand
- @type calendar: L{Component}
- @param event_details: list to append VEVENT components to
- @type event_details: C{list}
- @param timerange: the time-range in which to expand
- @type timerange: L{TimeRange}
- @param tzinfo: timezone for floating time calculations
- @type tzinfo: L{Timezone}
- """
-
- # 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{Timezone} for the timezone to use for floating/all-day events.
- """
-
- # Expand out the set of instances for the event with in the required range
- instances = calendar.expandTimeRanges(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 = Period(fbstart, duration=fbend - fbstart)
- else:
- period = Period(fbstart, fbend)
- clipped = clipPeriod(period, Period(timerange.start, timerange.end))
-
- # Double check for overlap
- if clipped:
- if status == "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(), Period(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 = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- end = vav.getEndDateUTC()
- if end is None:
- end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- period = Period(start, end)
- overall = clipPeriod(period, Period(timerange.start, timerange.end))
- if overall is None:
- continue
-
- # Now get periods for each instance of AVAILABLE sub-components
- periods = processAvailablePeriods(vav, timerange)
-
- # Now invert the periods and store in accumulator
- busyperiods = []
- last_end = timerange.start
- for period in periods:
- if last_end < period.getStart():
- busyperiods.append(Period(last_end, period.getStart()))
- last_end = period.getEnd()
- if last_end < timerange.end:
- busyperiods.append(Period(last_end, timerange.end))
-
- # Add to actual results mapped by busy type
- fbtype = vav.propertyValue("BUSYTYPE")
- if fbtype is None:
- fbtype = "BUSY-UNAVAILABLE"
-
- fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
-
-
-
-def processAvailablePeriods(calendar, timerange):
- """
- Extract instance period data from an AVAILABLE component.
- @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
- @param timerange: the time range to restrict free busy data to.
- """
-
- periods = []
-
- # First we need to group all AVAILABLE sub-components by UID
- uidmap = {}
- for component in calendar.subcomponents():
- if component.name() == "AVAILABLE":
- uid = component.propertyValue("UID")
- uidmap.setdefault(uid, []).append(component)
-
- # Then we expand each uid set separately
- for componentSet in uidmap.itervalues():
- instances = InstanceList(ignoreInvalidInstances=True)
- instances.expandTimeRanges(componentSet, timerange.end)
-
- # Now convert instances into period list
- for key in instances:
- instance = instances[key]
- # Ignore any with floating times (which should not happen as the spec requires UTC or local
- # but we will try and be safe here).
- start = instance.start
- if start.floating():
- continue
- end = instance.end
- if end.floating():
- continue
-
- # Clip period for this instance - use duration for period end if that
- # is what original component used
- if instance.component.hasProperty("DURATION"):
- period = Period(start, duration=end - start)
- else:
- period = Period(start, end)
- clipped = clipPeriod(period, Period(timerange.start, timerange.end))
- if clipped:
- periods.append(clipped)
-
- normalizePeriodList(periods)
- return periods
-
-
-
-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", DateTime.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 = md5(str(fbcalendar) + str(time.time())).hexdigest()
- fb.addProperty(Property("UID", uid))
-
- if event_details:
- for vevent in event_details:
- fbcalendar.addComponent(vevent)
-
- return fbcalendar
</del></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethodreport_freebusypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/method/report_freebusy.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/method/report_freebusy.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/method/report_freebusy.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -20,9 +20,10 @@
</span><span class="cx">
</span><span class="cx"> __all__ = ["report_urn_ietf_params_xml_ns_caldav_free_busy_query"]
</span><span class="cx">
</span><del>-from twisted.internet.defer import inlineCallbacks, returnValue
</del><ins>+from twext.python.log import Logger
</ins><span class="cx">
</span><del>-from twext.python.log import Logger
</del><ins>+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
</ins><span class="cx"> from txweb2 import responsecode
</span><span class="cx"> from txweb2.dav.http import ErrorResponse
</span><span class="cx"> from txweb2.dav.method.report import NumberOfMatchesWithinLimits
</span><span class="lines">@@ -36,12 +37,15 @@
</span><span class="cx"> from twistedcaldav.util import bestAcceptType
</span><span class="cx">
</span><span class="cx"> from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
</span><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.xml import element as davxml
</span><span class="cx">
</span><ins>+from pycalendar.period import Period
+
</ins><span class="cx"> log = Logger()
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><del>-def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): #@UnusedVariable
</del><ins>+def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy):
</ins><span class="cx"> """
</span><span class="cx"> Generate a free-busy REPORT.
</span><span class="cx"> (CalDAV-access-09, section 7.8)
</span><span class="lines">@@ -57,36 +61,36 @@
</span><span class="cx"> if not timerange.valid():
</span><span class="cx"> raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified"))
</span><span class="cx">
</span><del>- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
</del><ins>+ fbset = []
</ins><span class="cx">
</span><del>- matchcount = [0]
-
</del><span class="cx"> accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
</span><span class="cx"> if accepted_type is None:
</span><span class="cx"> raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
</span><span class="cx">
</span><span class="cx">
</span><del>- def generateFreeBusyInfo(calresource, uri): #@UnusedVariable
</del><ins>+ def getCalendarList(calresource, uri): #@UnusedVariable
</ins><span class="cx"> """
</span><del>- Run a free busy report on the specified calendar collection
- accumulating the free busy info for later processing.
</del><ins>+ Store the calendars that match the query in L{fbset} which will then be used with the
+ freebusy query.
+
</ins><span class="cx"> @param calresource: the L{CalDAVResource} for a calendar collection.
</span><del>- @param uri: the uri for the calendar collecton resource.
</del><ins>+ @param uri: the uri for the calendar collection resource.
</ins><span class="cx"> """
</span><span class="cx">
</span><del>- def _gotResult(result):
- matchcount[0] = result
- return True
</del><ins>+ fbset.append(calresource._newStoreObject)
+ return succeed(True)
</ins><span class="cx">
</span><del>- d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0])
- d.addCallback(_gotResult)
- return d
</del><ins>+ # Run report taking depth into account
+ depth = request.headers.getHeader("depth", "0")
+ yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),))
</ins><span class="cx">
</span><del>- # Run report taking depth into account
</del><ins>+ # Do the actual freebusy query against the set of matched calendars
+ timerange = Period(timerange.start, timerange.end)
</ins><span class="cx"> try:
</span><del>- depth = request.headers.getHeader("depth", "0")
- yield report_common.applyToCalendarCollections(self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(),))
</del><ins>+ fbresult = (yield FreebusyQuery(
+ None, None, None, None, None,
+ timerange, None, None,
+ ).generateAttendeeFreeBusyResponse(fbset=fbset, method=None))
</ins><span class="cx"> except NumberOfMatchesWithinLimits:
</span><span class="cx"> log.error("Too many matching components in free-busy report")
</span><span class="cx"> raise HTTPError(ErrorResponse(
</span><span class="lines">@@ -107,11 +111,8 @@
</span><span class="cx"> "Time-range value too far in the future. Must be on or before %s." % (str(e.limit),)
</span><span class="cx"> ))
</span><span class="cx">
</span><del>- # Now build a new calendar object with the free busy info we have
- fbcalendar = report_common.buildFreeBusyResult(fbinfo, timerange)
-
</del><span class="cx"> response = Response()
</span><del>- response.stream = MemoryStream(fbcalendar.getText(accepted_type))
</del><ins>+ response.stream = MemoryStream(fbresult.getText(accepted_type))
</ins><span class="cx"> response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
</span><span class="cx">
</span><span class="cx"> returnValue(response)
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingcaldavdeliverypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -26,10 +26,8 @@
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx">
</span><span class="cx"> from txdav.base.propertystore.base import PropertyName
</span><del>-from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser, OtherServerCalendarUser
</del><span class="cx"> from txdav.caldav.datastore.scheduling.delivery import DeliveryService
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import processAvailabilityFreeBusy, \
- generateFreeBusyInfo, buildFreeBusyResult
</del><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
</span><span class="cx"> from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
</span><span class="cx"> from txdav.caldav.datastore.scheduling.utils import extractEmailDomain
</span><span class="lines">@@ -97,10 +95,6 @@
</span><span class="cx"> organizerProp = self.scheduler.calendar.getOrganizerProperty()
</span><span class="cx"> uid = self.scheduler.calendar.resourceUID()
</span><span class="cx">
</span><del>- organizerPrincipal = None
- if type(self.scheduler.organizer) in (LocalCalendarUser, OtherServerCalendarUser,):
- organizerPrincipal = self.scheduler.organizer.record.uid
-
</del><span class="cx"> for recipient in self.recipients:
</span><span class="cx">
</span><span class="cx"> #
</span><span class="lines">@@ -113,7 +107,7 @@
</span><span class="cx"> # Look for special delegate extended free-busy request
</span><span class="cx"> event_details = [] if self.scheduler.calendar.getExtendedFreeBusy() else None
</span><span class="cx">
</span><del>- yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid, event_details)
</del><ins>+ yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, uid, event_details)
</ins><span class="cx"> else:
</span><span class="cx"> yield self.generateResponse(recipient, self.responses)
</span><span class="cx">
</span><span class="lines">@@ -199,32 +193,26 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><del>- def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details):
</del><ins>+ def generateFreeBusyResponse(self, recipient, responses, organizerProp, uid, event_details):
</ins><span class="cx">
</span><span class="cx"> # Extract the ATTENDEE property matching current recipient from the calendar data
</span><span class="cx"> cuas = recipient.record.calendarUserAddresses
</span><span class="cx"> attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
</span><span class="cx">
</span><del>- remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
-
</del><span class="cx"> try:
</span><del>- fbresult = (yield self.generateAttendeeFreeBusyResponse(
- recipient,
- organizerProp,
- organizerPrincipal,
- uid,
- attendeeProp,
- remote,
- event_details,
- ))
- except Exception:
</del><ins>+ fbresult = (yield FreebusyQuery(
+ self.scheduler.organizer, organizerProp, recipient, attendeeProp, uid,
+ self.scheduler.timeRange, self.scheduler.excludeUID, self.scheduler.logItems,
+ event_details=event_details,
+ ).generateAttendeeFreeBusyResponse())
+ except Exception as e:
</ins><span class="cx"> log.failure(
</span><span class="cx"> "Could not determine free busy information for recipient {cuaddr}",
</span><span class="cx"> cuaddr=recipient.cuaddr, level=LogLevel.debug
</span><span class="cx"> )
</span><span class="cx"> log.error(
</span><del>- "Could not determine free busy information for recipient {cuaddr}",
- cuaddr=recipient.cuaddr
</del><ins>+ "Could not determine free busy information for recipient {cuaddr}: {ex}",
+ cuaddr=recipient.cuaddr, ex=e
</ins><span class="cx"> )
</span><span class="cx"> err = HTTPError(ErrorResponse(
</span><span class="cx"> responsecode.FORBIDDEN,
</span><span class="lines">@@ -245,56 +233,3 @@
</span><span class="cx"> calendar=fbresult
</span><span class="cx"> )
</span><span class="cx"> returnValue(True)
</span><del>-
-
- @inlineCallbacks
- def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):
-
- # Find the current recipients calendars that are not transparent
- fbset = (yield recipient.inbox.ownerHome().loadCalendars())
- fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- # Process the availability property from the Inbox.
- availability = recipient.inbox.ownerHome().getAvailability()
- if availability is not None:
- processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
-
- # Check to see if the recipient is the same calendar user as the organizer.
- # Needed for masked UID stuff.
- if isinstance(self.scheduler.organizer, LocalCalendarUser):
- same_calendar_user = self.scheduler.organizer.record.uid == recipient.record.uid
- else:
- same_calendar_user = False
-
- # Now process free-busy set calendars
- matchtotal = 0
- for calendar in fbset:
- matchtotal = (yield generateFreeBusyInfo(
- calendar,
- fbinfo,
- self.scheduler.timeRange,
- matchtotal,
- excludeuid=self.scheduler.excludeUID,
- organizer=self.scheduler.organizer.cuaddr,
- organizerPrincipal=organizerPrincipal,
- same_calendar_user=same_calendar_user,
- servertoserver=remote,
- event_details=event_details,
- logItems=self.scheduler.logItems,
- ))
-
- # Build VFREEBUSY iTIP reply for this recipient
- fbresult = buildFreeBusyResult(
- fbinfo,
- self.scheduler.timeRange,
- organizer=organizerProp,
- attendee=attendeeProp,
- uid=uid,
- method="REPLY",
- event_details=event_details,
- )
-
- returnValue(fbresult)
</del></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingfreebusypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -35,28 +35,26 @@
</span><span class="cx">
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><span class="cx"> from txdav.caldav.icalendarstore import QueryMaxResources
</span><ins>+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
</ins><span class="cx"> from txdav.common.icommondatastore import IndexedSearchException, \
</span><span class="cx"> InternalDataStoreError
</span><span class="cx">
</span><span class="cx"> import uuid
</span><ins>+from collections import namedtuple
</ins><span class="cx">
</span><span class="cx"> log = Logger()
</span><span class="cx">
</span><del>-fbtype_mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
-fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
</del><span class="cx">
</span><del>-fbcacher = Memcacher("FBCache", pickle=True)
-
-
-
</del><span class="cx"> class FBCacheEntry(object):
</span><span class="cx">
</span><span class="cx"> CACHE_DAYS_FLOATING_ADJUST = 1
</span><span class="cx">
</span><ins>+ fbcacher = Memcacher("FBCache", pickle=True)
+
</ins><span class="cx"> def __init__(self, key, token, timerange, fbresults):
</span><span class="cx"> self.key = key
</span><span class="cx"> self.token = token
</span><del>- self.timerange = timerange
</del><ins>+ self.timerange = timerange.getText()
</ins><span class="cx"> self.fbresults = fbresults
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -66,16 +64,17 @@
</span><span class="cx">
</span><span class="cx"> key = str(calresource.id()) + "/" + useruid
</span><span class="cx"> token = (yield calresource.syncToken())
</span><del>- entry = (yield fbcacher.get(key))
</del><ins>+ entry = (yield cls.fbcacher.get(key))
</ins><span class="cx">
</span><span class="cx"> if entry:
</span><span class="cx">
</span><span class="cx"> # Offset one day at either end to account for floating
</span><del>- cached_start = entry.timerange.start + Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
- cached_end = entry.timerange.end - Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
</del><ins>+ entry_timerange = Period.parseText(entry.timerange)
+ cached_start = entry_timerange.getStart() + Duration(days=cls.CACHE_DAYS_FLOATING_ADJUST)
+ cached_end = entry_timerange.getEnd() - Duration(days=cls.CACHE_DAYS_FLOATING_ADJUST)
</ins><span class="cx">
</span><span class="cx"> # Verify that the requested time range lies within the cache time range
</span><del>- if compareDateTime(timerange.end, cached_end) <= 0 and compareDateTime(timerange.start, cached_start) >= 0:
</del><ins>+ if compareDateTime(timerange.getEnd(), cached_end) <= 0 and compareDateTime(timerange.getStart(), cached_start) >= 0:
</ins><span class="cx">
</span><span class="cx"> # Verify that cached entry is still valid
</span><span class="cx"> if token == entry.token:
</span><span class="lines">@@ -91,693 +90,700 @@
</span><span class="cx"> key = str(calresource.id()) + "/" + useruid
</span><span class="cx"> token = (yield calresource.syncToken())
</span><span class="cx"> entry = cls(key, token, timerange, fbresults)
</span><del>- yield fbcacher.set(key, entry)
</del><ins>+ yield cls.fbcacher.set(key, entry)
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-def generateFreeBusyInfo(
- calresource,
- fbinfo,
- timerange,
- matchtotal,
- excludeuid=None,
- organizer=None,
- organizerPrincipal=None,
- same_calendar_user=False,
- servertoserver=False,
- event_details=None,
- logItems=None,
- accountingItems=None,
-):
</del><ins>+class FreebusyQuery(object):
</ins><span class="cx"> """
</span><del>- Get freebusy information for a calendar. Different behavior for internal vs external calendars.
-
- See L{_internalGenerateFreeBusyInfo} for argument description.
</del><ins>+ Class that manages the process of getting free busy information of a particular attendee.
</ins><span class="cx"> """
</span><span class="cx">
</span><del>- # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
- # can be split across L{CalendarObject} and L{CalendarObjectExternal}
- if calresource.external():
- return _externalGenerateFreeBusyInfo(
- calresource,
- fbinfo,
- timerange,
- matchtotal,
- excludeuid,
- organizer,
- organizerPrincipal,
- same_calendar_user,
- servertoserver,
- event_details,
- logItems,
- accountingItems,
- )
- else:
- return _internalGenerateFreeBusyInfo(
- calresource,
- fbinfo,
- timerange,
- matchtotal,
- excludeuid,
- organizer,
- organizerPrincipal,
- same_calendar_user,
- servertoserver,
- event_details,
- logItems,
- accountingItems,
- )
</del><ins>+ FBInfo = namedtuple("FBInfo", ("busy", "tentative", "unavailable",))
+ FBInfo_mapper = {"BUSY": "busy", "BUSY-TENTATIVE": "tentative", "BUSY-UNAVAILABLE": "unavailable"}
+ FBInfo_index_mapper = {'B': "busy", 'T': "tentative", 'U': "unavailable"}
</ins><span class="cx">
</span><ins>+ def __init__(self, organizer, organizerProp, recipient, attendeeProp, uid, timeRange, excludeUID, logItems, accountingItems=None, event_details=None):
+ """
</ins><span class="cx">
</span><ins>+ @param organizer: the organizer making the freebusy request
+ @type organizer: L{CalendarUser}
+ @param organizerProp: iCalendar ORGANIZER property from the request
+ @type organizerProp: L{Property}
+ @param recipient: the attendee whose freebusy is being requested
+ @type recipient: L{CalendarUser}
+ @param attendeeProp: iCalendar ATTENDEE property from the request
+ @type attendeeProp: L{Property}
+ @param uid: iCalendar UID in the request
+ @type uid: L{str}
+ @param timeRange: time range for freebusy request
+ @type timeRange: L{Period}
+ @param excludeUID: an iCalendar UID to exclude from busy results
+ @type excludeUID: L{str}
+ @param logItems: items to add to logging
+ @type logItems: L{dict}
+ @param accountingItems: items to add to accounting logging
+ @type accountingItems: L{dict}
+ @param event_details: if not L{None}, a list in which busy event details are stored
+ @type event_details: L{list} or L{None}
+ """
+ self.organizer = organizer
+ self.organizerProp = organizerProp
+ self.recipient = recipient
+ self.attendeeProp = attendeeProp
+ self.uid = uid
+ self.timerange = timeRange
+ self.excludeuid = excludeUID
+ self.logItems = logItems
+ self.accountingItems = accountingItems
+ self.event_details = event_details
</ins><span class="cx">
</span><del>-@inlineCallbacks
-def _externalGenerateFreeBusyInfo(
- calresource,
- fbinfo,
- timerange,
- matchtotal,
- excludeuid=None,
- organizer=None,
- organizerPrincipal=None,
- same_calendar_user=False,
- servertoserver=False,
- event_details=None,
- logItems=None,
- accountingItems=None,
-):
- """
- Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
- any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
</del><ins>+ # Check to see if the recipient is the same calendar user as the organizer.
+ # Needed for masked UID stuff.
+ if isinstance(self.organizer, LocalCalendarUser):
+ self.same_calendar_user = self.organizer.record.uid == self.recipient.record.uid
+ else:
+ self.same_calendar_user = False
</ins><span class="cx">
</span><del>- See L{_internalGenerateFreeBusyInfo} for argument description.
- """
- fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid, organizer, organizerPrincipal, same_calendar_user, servertoserver, event_details)
- for i in range(3):
- fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
- returnValue(matchtotal)
</del><span class="cx">
</span><ins>+ @inlineCallbacks
+ def generateAttendeeFreeBusyResponse(self, fbset=None, method="REPLY"):
</ins><span class="cx">
</span><ins>+ # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+ fbinfo = self.FBInfo([], [], [])
</ins><span class="cx">
</span><del>-@inlineCallbacks
-def _internalGenerateFreeBusyInfo(
- calresource,
- fbinfo,
- timerange,
- matchtotal,
- excludeuid=None,
- organizer=None,
- organizerPrincipal=None,
- same_calendar_user=False,
- servertoserver=False,
- event_details=None,
- logItems=None,
- accountingItems=None,
-):
- """
- Run a free busy report on the specified calendar collection
- accumulating the free busy info for later processing.
- @param calresource: the L{Calendar} for a calendar collection.
- @param fbinfo: the array of busy periods to update.
- @param timerange: the L{TimeRange} for the query.
- @param matchtotal: the running total for the number of matches.
- @param excludeuid: a C{str} containing a UID value to exclude any
- components with that UID from contributing to free-busy.
- @param organizer: a C{str} containing the value of the ORGANIZER property
- in the VFREEBUSY request. This is used in conjunction with the UID
- value to process exclusions.
- @param same_calendar_user: a C{bool} indicating whether the calendar user
- requesting the free-busy information is the same as the calendar user
- being targeted.
- @param servertoserver: a C{bool} indicating whether we are doing a local or
- remote lookup request.
- @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
- @param logItems: a C{dict} to store logging info to
- @param accountingItems: a C{dict} to store accounting info to
- """
</del><ins>+ if self.recipient is not None:
+ # Find the current recipients calendars that are not transparent
+ if fbset is None:
+ fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
+ fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]
</ins><span class="cx">
</span><del>- # First check the privilege on this collection
- # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
- # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy
</del><ins>+ # Process the availability property from the Inbox.
+ availability = self.recipient.inbox.ownerHome().getAvailability()
+ if availability is not None:
+ self.processAvailabilityFreeBusy(availability, fbinfo)
</ins><span class="cx">
</span><del>- # May need organizer principal
- organizer_record = (yield calresource.directoryService().recordWithCalendarUserAddress(organizer)) if organizer else None
- organizer_uid = organizer_record.uid if organizer_record else ""
</del><ins>+ # Now process free-busy set calendars
+ matchtotal = 0
+ for calendar in fbset:
+ matchtotal = (yield self.generateFreeBusyInfo(
+ calendar,
+ fbinfo,
+ matchtotal,
+ ))
</ins><span class="cx">
</span><del>- # Free busy is per-user
- attendee_uid = calresource.viewerHome().uid()
- attendee_record = yield calresource.directoryService().recordWithUID(attendee_uid.decode("utf-8"))
</del><ins>+ # Build VFREEBUSY iTIP reply for this recipient
+ fbresult = self.buildFreeBusyResult(fbinfo, method=method)
</ins><span class="cx">
</span><del>- # Get the timezone property from the collection.
- tz = calresource.getTimezone()
</del><ins>+ returnValue(fbresult)
</ins><span class="cx">
</span><del>- # Look for possible extended free busy information
- rich_options = {
- "organizer": False,
- "delegate": False,
- "resource": False,
- }
- do_event_details = False
- if event_details is not None and organizer_record is not None and attendee_record is not None:
</del><span class="cx">
</span><del>- # Get the principal of the authorized user which may be different from the organizer if a delegate of
- # the organizer is making the request
- authz_uid = organizer_uid
- authz_record = organizer_record
- if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid:
- authz_uid = calresource._txn._authz_uid
- authz_record = yield calresource.directoryService().recordWithUID(authz_uid.decode("utf-8"))
</del><ins>+ def processAvailabilityFreeBusy(self, calendar, fbinfo):
+ """
+ Extract free-busy data from a VAVAILABILITY component.
</ins><span class="cx">
</span><del>- # Check if attendee is also the organizer or the delegate doing the request
- if attendee_uid in (organizer_uid, authz_uid):
- do_event_details = True
- rich_options["organizer"] = True
</del><ins>+ @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
+ @param fbinfo: the tuple used to store the three types of fb data.
+ """
</ins><span class="cx">
</span><del>- # Check if authorized user is a delegate of attendee
- proxy = (yield authz_record.isProxyFor(attendee_record))
- if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
- do_event_details = True
- rich_options["delegate"] = True
</del><ins>+ for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:
</ins><span class="cx">
</span><del>- # Check if attendee is room or resource
- if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in ("RESOURCE", "ROOM",):
- do_event_details = True
- rich_options["resource"] = True
</del><ins>+ # Get overall start/end
+ start = vav.getStartDateUTC()
+ if start is None:
+ start = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
+ end = vav.getEndDateUTC()
+ if end is None:
+ end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
+ period = Period(start, end)
+ overall = clipPeriod(period, self.timerange)
+ if overall is None:
+ continue
</ins><span class="cx">
</span><del>- # Try cache
- resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, timerange)) if config.EnableFreeBusyCache else None
</del><ins>+ # Now get periods for each instance of AVAILABLE sub-components
+ periods = self.processAvailablePeriods(vav)
</ins><span class="cx">
</span><del>- if resources is None:
</del><ins>+ # Now invert the periods and store in accumulator
+ busyperiods = []
+ last_end = self.timerange.getStart()
+ for period in periods:
+ if last_end < period.getStart():
+ busyperiods.append(Period(last_end, period.getStart()))
+ last_end = period.getEnd()
+ if last_end < self.timerange.getEnd():
+ busyperiods.append(Period(last_end, self.timerange.getEnd()))
</ins><span class="cx">
</span><del>- if accountingItems is not None:
- accountingItems["fb-uncached"] = accountingItems.get("fb-uncached", 0) + 1
</del><ins>+ # Add to actual results mapped by busy type
+ fbtype = vav.propertyValue("BUSYTYPE")
+ if fbtype is None:
+ fbtype = "BUSY-UNAVAILABLE"
</ins><span class="cx">
</span><del>- caching = False
- if config.EnableFreeBusyCache:
- # Log extended item
- if logItems is not None:
- logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1
</del><ins>+ getattr(fbinfo, self.FBInfo_mapper.get(fbtype, "unavailable")).extend(busyperiods)
</ins><span class="cx">
</span><del>- # We want to cache a large range of time based on the current date
- cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
- cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
</del><span class="cx">
</span><del>- # If the requested time range would fit in our allowed cache range, trigger the cache creation
- if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime(timerange.end, cache_end) <= 0:
- cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText())
- caching = True
</del><ins>+ def processAvailablePeriods(self, calendar):
+ """
+ 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.
+ """
</ins><span class="cx">
</span><del>- #
- # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
- # We then take those results and merge them into one VFREEBUSY component
- # with appropriate FREEBUSY properties, and return that single item as iCal data.
- #
</del><ins>+ periods = []
</ins><span class="cx">
</span><del>- # Create fake filter element to match time-range
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- caldavxml.ComponentFilter(
- cache_timerange if caching else timerange,
- name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
- ),
- name="VCALENDAR",
- )
- )
- filter = Filter(filter)
- tzinfo = filter.settimezone(tz)
- if accountingItems is not None:
- tr = cache_timerange if caching else timerange
- accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),)
</del><ins>+ # First we need to group all AVAILABLE sub-components by UID
+ uidmap = {}
+ for component in calendar.subcomponents():
+ if component.name() == "AVAILABLE":
+ uid = component.propertyValue("UID")
+ uidmap.setdefault(uid, []).append(component)
</ins><span class="cx">
</span><del>- try:
- resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True)
- if caching:
- yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
- except IndexedSearchException:
- raise InternalDataStoreError("Invalid indexedSearch query")
</del><ins>+ # Then we expand each uid set separately
+ for componentSet in uidmap.itervalues():
+ instances = InstanceList(ignoreInvalidInstances=True)
+ instances.expandTimeRanges(componentSet, self.timerange.getEnd())
</ins><span class="cx">
</span><del>- else:
- if accountingItems is not None:
- accountingItems["fb-cached"] = accountingItems.get("fb-cached", 0) + 1
</del><ins>+ # Now convert instances into period list
+ for key in instances:
+ instance = instances[key]
+ # Ignore any with floating times (which should not happen as the spec requires UTC or local
+ # but we will try and be safe here).
+ start = instance.start
+ if start.floating():
+ continue
+ end = instance.end
+ if end.floating():
+ continue
</ins><span class="cx">
</span><del>- # Log extended item
- if logItems is not None:
- logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1
</del><ins>+ # Clip period for this instance - use duration for period end if that
+ # is what original component used
+ if instance.component.hasProperty("DURATION"):
+ period = Period(start, duration=end - start)
+ else:
+ period = Period(start, end)
+ clipped = clipPeriod(period, self.timerange)
+ if clipped:
+ periods.append(clipped)
</ins><span class="cx">
</span><del>- # Determine appropriate timezone (UTC is the default)
- tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
</del><ins>+ normalizePeriodList(periods)
+ return periods
</ins><span class="cx">
</span><del>- # We care about separate instances for VEVENTs only
- aggregated_resources = {}
- for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
- if transp == 'T' and fbtype != '?':
- fbtype = 'F'
- aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
</del><span class="cx">
</span><del>- if accountingItems is not None:
- accountingItems["fb-resources"] = {}
- for k, v in aggregated_resources.items():
- name, uid, type, test_organizer = k
- accountingItems["fb-resources"][uid] = []
- for float, start, end, fbtype in v:
- fbstart = parseSQLTimestampToPyCalendar(start)
- if float == 'Y':
- fbstart.setTimezone(tzinfo)
- else:
- fbstart.setTimezone(Timezone(utc=True))
- fbend = parseSQLTimestampToPyCalendar(end)
- if float == 'Y':
- fbend.setTimezone(tzinfo)
- else:
- fbend.setTimezone(Timezone(utc=True))
- accountingItems["fb-resources"][uid].append((
- float,
- str(fbstart),
- str(fbend),
- fbtype,
- ))
</del><ins>+ def generateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+ """
+ Get freebusy information for a calendar. Different behavior for internal vs external calendars.
</ins><span class="cx">
</span><del>- # Cache directory record lookup outside this loop as it is expensive and will likely
- # always end up being called with the same organizer address.
- recordUIDCache = {}
- for key in aggregated_resources.iterkeys():
</del><ins>+ See L{_internalGenerateFreeBusyInfo} for argument description.
+ """
</ins><span class="cx">
</span><del>- name, uid, type, test_organizer = key
</del><ins>+ # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
+ # can be split across L{CalendarObject} and L{CalendarObjectExternal}
+ if calresource.external():
+ return self._externalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ matchtotal,
+ )
+ else:
+ return self._internalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ matchtotal,
+ )
</ins><span class="cx">
</span><del>- # Short-cut - if an fbtype exists we can use that
- if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
</del><span class="cx">
</span><del>- matchedResource = False
</del><ins>+ @inlineCallbacks
+ def _externalGenerateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+ """
+ Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
+ any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
</ins><span class="cx">
</span><del>- # Look at each instance
- for float, start, end, fbtype in aggregated_resources[key]:
- # Ignore free time or unknown
- if fbtype in ('F', '?'):
- continue
</del><ins>+ See L{_internalGenerateFreeBusyInfo} for argument description.
+ """
+ fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(
+ calresource, self.organizer.cuaddr, self.recipient.cuaddr, self.timerange,
+ matchtotal, self.excludeuid, self.event_details
+ )
+ for i in range(3):
+ fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
+ returnValue(matchtotal)
</ins><span class="cx">
</span><del>- # Ignore ones of this UID
- if excludeuid:
- # See if we have a UID match
- if (excludeuid == uid):
- if test_organizer:
- test_uid = recordUIDCache.get(test_organizer)
- if test_uid is None:
- test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
- test_uid = test_record.uid if test_record else ""
- recordUIDCache[test_organizer] = test_uid
- else:
- test_uid = ""
</del><span class="cx">
</span><del>- # Check that ORGANIZER's match (security requirement)
- if (organizer is None) or (organizer_uid == test_uid):
- continue
- # Check for no ORGANIZER and check by same calendar user
- elif (test_uid == "") and same_calendar_user:
- continue
</del><ins>+ @inlineCallbacks
+ def _internalGenerateFreeBusyInfo(
+ self,
+ calresource,
+ fbinfo,
+ matchtotal,
+ ):
+ """
+ Run a free busy report on the specified calendar collection
+ accumulating the free busy info for later processing.
+ @param calresource: the L{Calendar} for a calendar collection.
+ @param fbinfo: the array of busy periods to update.
+ @param matchtotal: the running total for the number of matches.
+ """
</ins><span class="cx">
</span><del>- # Apply a timezone to any floating times
- fbstart = parseSQLTimestampToPyCalendar(start)
- if float == 'Y':
- fbstart.setTimezone(tzinfo)
- else:
- fbstart.setTimezone(Timezone(utc=True))
- fbend = parseSQLTimestampToPyCalendar(end)
- if float == 'Y':
- fbend.setTimezone(tzinfo)
- else:
- fbend.setTimezone(Timezone(utc=True))
</del><ins>+ # First check the privilege on this collection
+ # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+ # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy
</ins><span class="cx">
</span><del>- # Clip instance to time range
- clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end))
</del><ins>+ # May need organizer principal
+ organizer_record = self.organizer.record if self.organizer and self.organizer.hosted() else None
+ organizer_uid = organizer_record.uid if organizer_record else ""
</ins><span class="cx">
</span><del>- # Double check for overlap
- if clipped:
- matchedResource = True
- fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)
</del><ins>+ # Free busy is per-user
+ attendee_record = self.recipient.record if self.organizer and self.recipient.hosted() else None
+ attendee_uid = attendee_record.uid if attendee_record else calresource.viewerHome().uid()
</ins><span class="cx">
</span><del>- if matchedResource:
- # Check size of results is within limit
- matchtotal += 1
- if matchtotal > config.MaxQueryWithDataResults:
- raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</del><ins>+ # Get the timezone property from the collection.
+ tz = calresource.getTimezone()
</ins><span class="cx">
</span><del>- # Add extended details
- if do_event_details:
- child = (yield calresource.calendarObjectWithName(name))
- # Only add fully public events
- if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
- calendar = (yield child.componentForUser())
- _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
</del><ins>+ # Look for possible extended free busy information
+ rich_options = {
+ "organizer": False,
+ "delegate": False,
+ "resource": False,
+ }
+ do_event_details = False
+ if self.event_details is not None and organizer_record is not None and attendee_record is not None:
</ins><span class="cx">
</span><del>- else:
- child = (yield calresource.calendarObjectWithName(name))
- calendar = (yield child.componentForUser())
</del><ins>+ # Get the principal of the authorized user which may be different from the organizer if a delegate of
+ # the organizer is making the request
+ authz_uid = organizer_uid
+ authz_record = organizer_record
+ if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid:
+ authz_uid = calresource._txn._authz_uid
+ authz_record = yield calresource.directoryService().recordWithUID(authz_uid.decode("utf-8"))
</ins><span class="cx">
</span><del>- # The calendar may come back as None if the resource is being changed, or was deleted
- # between our initial index query and getting here. For now we will ignore this error, but in
- # the longer term we need to implement some form of locking, perhaps.
- if calendar is None:
- log.error("Calendar %s is missing from calendar collection %r" % (name, calresource))
- continue
</del><ins>+ # Check if attendee is also the organizer or the delegate doing the request
+ if attendee_uid in (organizer_uid, authz_uid):
+ do_event_details = True
+ rich_options["organizer"] = True
</ins><span class="cx">
</span><del>- # Ignore ones of this UID
- if excludeuid:
- # See if we have a UID match
- if (excludeuid == uid):
- test_organizer = calendar.getOrganizer()
- if test_organizer:
- test_uid = recordUIDCache.get(test_organizer)
- if test_uid is None:
- test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
- test_uid = test_record.uid if test_record else ""
- recordUIDCache[test_organizer] = test_uid
- else:
- test_uid = ""
</del><ins>+ # Check if authorized user is a delegate of attendee
+ proxy = (yield authz_record.isProxyFor(attendee_record))
+ if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
+ do_event_details = True
+ rich_options["delegate"] = True
</ins><span class="cx">
</span><del>- # Check that ORGANIZER's match (security requirement)
- if (organizer is None) or (organizer_uid == test_uid):
- continue
- # Check for no ORGANIZER and check by same calendar user
- elif (test_organizer is None) and same_calendar_user:
- continue
</del><ins>+ # Check if attendee is room or resource
+ if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in ("RESOURCE", "ROOM",):
+ do_event_details = True
+ rich_options["resource"] = True
</ins><span class="cx">
</span><del>- if accountingItems is not None:
- accountingItems.setdefault("fb-filter-match", []).append(uid)
</del><ins>+ # Try cache
+ resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None
</ins><span class="cx">
</span><del>- if filter.match(calendar, None):
- if accountingItems is not None:
- accountingItems.setdefault("fb-filter-matched", []).append(uid)
</del><ins>+ if resources is None:
</ins><span class="cx">
</span><del>- # Check size of results is within limit
- matchtotal += 1
- if matchtotal > config.MaxQueryWithDataResults:
- raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</del><ins>+ if self.accountingItems is not None:
+ self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1
</ins><span class="cx">
</span><del>- 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,)
</del><ins>+ caching = False
+ if config.EnableFreeBusyCache:
+ # Log extended item
+ if self.logItems is not None:
+ self.logItems["fb-uncached"] = self.logItems.get("fb-uncached", 0) + 1
</ins><span class="cx">
</span><del>- # Add extended details
- if calendar.mainType() == "VEVENT" and do_event_details:
- child = (yield calresource.calendarObjectWithName(name))
- # Only add fully public events
- if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
- calendar = (yield child.componentForUser())
- _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
</del><ins>+ # We want to cache a large range of time based on the current date
+ cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
+ cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
</ins><span class="cx">
</span><del>- returnValue(matchtotal)
</del><ins>+ # If the requested time range would fit in our allowed cache range, trigger the cache creation
+ if compareDateTime(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0:
+ cache_timerange = Period(cache_start, cache_end)
+ caching = True
</ins><span class="cx">
</span><ins>+ #
+ # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
+ # We then take those results and merge them into one VFREEBUSY component
+ # with appropriate FREEBUSY properties, and return that single item as iCal data.
+ #
</ins><span class="cx">
</span><ins>+ # Create fake filter element to match time-range
+ tr = TimeRange(
+ start=(cache_timerange if caching else self.timerange).getStart().getText(),
+ end=(cache_timerange if caching else self.timerange).getEnd().getText(),
+ )
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ tr,
+ name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+ ),
+ name="VCALENDAR",
+ )
+ )
+ filter = Filter(filter)
+ tzinfo = filter.settimezone(tz)
+ if self.accountingItems is not None:
+ self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),)
</ins><span class="cx">
</span><del>-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.
</del><ins>+ try:
+ resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True)
+ if caching:
+ yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
+ except IndexedSearchException:
+ raise InternalDataStoreError("Invalid indexedSearch query")
</ins><span class="cx">
</span><del>- @param calendar: the calendar object to expand
- @type calendar: L{Component}
- @param event_details: list to append VEVENT components to
- @type event_details: C{list}
- @param timerange: the time-range in which to expand
- @type timerange: L{TimeRange}
- @param tzinfo: timezone for floating time calculations
- @type tzinfo: L{Timezone}
- """
</del><ins>+ else:
+ if self.accountingItems is not None:
+ self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1
</ins><span class="cx">
</span><del>- # First expand the component
- expanded = calendar.expand(timerange.start, timerange.end, timezone=tzinfo)
</del><ins>+ # Log extended item
+ if self.logItems is not None:
+ self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1
</ins><span class="cx">
</span><del>- keep_props = (
- "UID",
- "RECURRENCE-ID",
- "DTSTAMP",
- "DTSTART",
- "DTEND",
- "DURATION",
- )
</del><ins>+ # Determine appropriate timezone (UTC is the default)
+ tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
</ins><span class="cx">
</span><del>- if rich_options["organizer"] or rich_options["delegate"]:
- keep_props += ("SUMMARY",)
</del><ins>+ # We care about separate instances for VEVENTs only
+ aggregated_resources = {}
+ for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
+ if transp == 'T' and fbtype != '?':
+ fbtype = 'F'
+ aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
</ins><span class="cx">
</span><del>- if rich_options["organizer"] or rich_options["resource"]:
- keep_props += ("ORGANIZER",)
</del><ins>+ if self.accountingItems is not None:
+ self.accountingItems["fb-resources"] = {}
+ for k, v in aggregated_resources.items():
+ name, uid, type, test_organizer = k
+ self.accountingItems["fb-resources"][uid] = []
+ for float, start, end, fbtype in v:
+ fbstart = parseSQLTimestampToPyCalendar(start)
+ if float == 'Y':
+ fbstart.setTimezone(tzinfo)
+ else:
+ fbstart.setTimezone(Timezone(utc=True))
+ fbend = parseSQLTimestampToPyCalendar(end)
+ if float == 'Y':
+ fbend.setTimezone(tzinfo)
+ else:
+ fbend.setTimezone(Timezone(utc=True))
+ self.accountingItems["fb-resources"][uid].append((
+ float,
+ str(fbstart),
+ str(fbend),
+ fbtype,
+ ))
</ins><span class="cx">
</span><del>- # Remove all but essential properties
- expanded.filterProperties(keep=keep_props)
</del><ins>+ # Cache directory record lookup outside this loop as it is expensive and will likely
+ # always end up being called with the same organizer address.
+ recordUIDCache = {}
+ for key in aggregated_resources.iterkeys():
</ins><span class="cx">
</span><del>- # Need to remove all child components of VEVENT
- for subcomponent in expanded.subcomponents():
- if subcomponent.name() == "VEVENT":
- for sub in tuple(subcomponent.subcomponents()):
- subcomponent.removeComponent(sub)
</del><ins>+ name, uid, type, test_organizer = key
</ins><span class="cx">
</span><del>- event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == "VEVENT"])
</del><ins>+ # Short-cut - if an fbtype exists we can use that
+ if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
</ins><span class="cx">
</span><ins>+ matchedResource = False
</ins><span class="cx">
</span><ins>+ # Look at each instance
+ for float, start, end, fbtype in aggregated_resources[key]:
+ # Ignore free time or unknown
+ if fbtype in ('F', '?'):
+ continue
</ins><span class="cx">
</span><del>-def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
- """
- Extract free busy data from a VEVENT component.
- @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
- @param fbinfo: the tuple used to store the three types of fb data.
- @param timerange: the time range to restrict free busy data to.
- @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
- """
</del><ins>+ # Ignore ones of this UID
+ if self.excludeuid:
+ # See if we have a UID match
+ if (self.excludeuid == uid):
+ if test_organizer:
+ test_uid = recordUIDCache.get(test_organizer)
+ if test_uid is None:
+ test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
+ test_uid = test_record.uid if test_record else ""
+ recordUIDCache[test_organizer] = test_uid
+ else:
+ test_uid = ""
</ins><span class="cx">
</span><del>- # Expand out the set of instances for the event with in the required range
- instances = calendar.expandTimeRanges(timerange.end, lowerLimit=timerange.start, ignoreInvalidInstances=True)
</del><ins>+ # Check that ORGANIZER's match (security requirement)
+ if (self.organizer is None) or (organizer_uid == test_uid):
+ continue
+ # Check for no ORGANIZER and check by same calendar user
+ elif (test_uid == "") and self.same_calendar_user:
+ continue
</ins><span class="cx">
</span><del>- # Can only do timed events
- for key in instances:
- instance = instances[key]
- if instance.start.isDateOnly():
- return
- break
- else:
- return
</del><ins>+ # Apply a timezone to any floating times
+ fbstart = parseSQLTimestampToPyCalendar(start)
+ if float == 'Y':
+ fbstart.setTimezone(tzinfo)
+ else:
+ fbstart.setTimezone(Timezone(utc=True))
+ fbend = parseSQLTimestampToPyCalendar(end)
+ if float == 'Y':
+ fbend.setTimezone(tzinfo)
+ else:
+ fbend.setTimezone(Timezone(utc=True))
</ins><span class="cx">
</span><del>- for key in instances:
- instance = instances[key]
</del><ins>+ # Clip instance to time range
+ clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), self.timerange)
</ins><span class="cx">
</span><del>- # Apply a timezone to any floating times
- fbstart = instance.start
- if fbstart.floating():
- fbstart.setTimezone(tzinfo)
- fbend = instance.end
- if fbend.floating():
- fbend.setTimezone(tzinfo)
</del><ins>+ # Double check for overlap
+ if clipped:
+ matchedResource = True
+ getattr(fbinfo, self.FBInfo_index_mapper.get(fbtype, "busy")).append(clipped)
</ins><span class="cx">
</span><del>- # 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
</del><ins>+ if matchedResource:
+ # Check size of results is within limit
+ matchtotal += 1
+ if matchtotal > config.MaxQueryWithDataResults:
+ raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</ins><span class="cx">
</span><del>- # Determine status
- if instance.component.hasProperty("STATUS"):
- status = instance.component.propertyValue("STATUS")
- else:
- status = "CONFIRMED"
</del><ins>+ # Add extended details
+ if do_event_details:
+ child = (yield calresource.calendarObjectWithName(name))
+ # Only add fully public events
+ if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
+ calendar = (yield child.componentForUser())
+ self._addEventDetails(calendar, rich_options, tzinfo)
</ins><span class="cx">
</span><del>- # Ignore cancelled
- if status == "CANCELLED":
- continue
</del><ins>+ else:
+ child = (yield calresource.calendarObjectWithName(name))
+ calendar = (yield child.componentForUser())
</ins><span class="cx">
</span><del>- # Clip period for this instance - use duration for period end if that
- # is what original component used
- if instance.component.hasProperty("DURATION"):
- period = Period(fbstart, duration=fbend - fbstart)
- else:
- period = Period(fbstart, fbend)
- clipped = clipPeriod(period, Period(timerange.start, timerange.end))
</del><ins>+ # The calendar may come back as None if the resource is being changed, or was deleted
+ # between our initial index query and getting here. For now we will ignore this error, but in
+ # the longer term we need to implement some form of locking, perhaps.
+ if calendar is None:
+ log.error("Calendar %s is missing from calendar collection %r" % (name, calresource))
+ continue
</ins><span class="cx">
</span><del>- # Double check for overlap
- if clipped:
- if status == "TENTATIVE":
- fbinfo[1].append(clipped)
- else:
- fbinfo[0].append(clipped)
</del><ins>+ # Ignore ones of this UID
+ if self.excludeuid:
+ # See if we have a UID match
+ if (self.excludeuid == uid):
+ test_organizer = calendar.getOrganizer()
+ if test_organizer:
+ test_uid = recordUIDCache.get(test_organizer)
+ if test_uid is None:
+ test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
+ test_uid = test_record.uid if test_record else ""
+ recordUIDCache[test_organizer] = test_uid
+ else:
+ test_uid = ""
</ins><span class="cx">
</span><ins>+ # Check that ORGANIZER's match (security requirement)
+ if (self.organizer is None) or (organizer_uid == test_uid):
+ continue
+ # Check for no ORGANIZER and check by same calendar user
+ elif (test_organizer is None) and self.same_calendar_user:
+ continue
</ins><span class="cx">
</span><ins>+ if self.accountingItems is not None:
+ self.accountingItems.setdefault("fb-filter-match", []).append(uid)
</ins><span class="cx">
</span><del>-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.
- """
</del><ins>+ if filter.match(calendar, None):
+ if self.accountingItems is not None:
+ self.accountingItems.setdefault("fb-filter-matched", []).append(uid)
</ins><span class="cx">
</span><del>- 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
</del><ins>+ # Check size of results is within limit
+ matchtotal += 1
+ if matchtotal > config.MaxQueryWithDataResults:
+ raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
</ins><span class="cx">
</span><del>- # 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
</del><ins>+ if calendar.mainType() == "VEVENT":
+ self.processEventFreeBusy(calendar, fbinfo, tzinfo)
+ elif calendar.mainType() == "VFREEBUSY":
+ self.processFreeBusyFreeBusy(calendar, fbinfo)
+ elif calendar.mainType() == "VAVAILABILITY":
+ self.processAvailabilityFreeBusy(calendar, fbinfo)
+ else:
+ assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
</ins><span class="cx">
</span><del>- # Look at each period in the property
- assert isinstance(fb.value(), list), "FREEBUSY property does not contain a list of values: %r" % (fb,)
- for period in fb.value():
- # Clip period for this instance
- clipped = clipPeriod(period.getValue(), Period(timerange.start, timerange.end))
- if clipped:
- fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
</del><ins>+ # Add extended details
+ if calendar.mainType() == "VEVENT" and do_event_details:
+ child = (yield calresource.calendarObjectWithName(name))
+ # Only add fully public events
+ if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
+ calendar = (yield child.componentForUser())
+ self._addEventDetails(calendar, rich_options, tzinfo)
</ins><span class="cx">
</span><ins>+ returnValue(matchtotal)
</ins><span class="cx">
</span><span class="cx">
</span><del>-def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
- """
- 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.
- """
</del><ins>+ def _addEventDetails(self, calendar, rich_options, tzinfo):
+ """
+ Expand events within the specified time range and limit the set of properties to those allowed for
+ delegate extended free busy.
</ins><span class="cx">
</span><del>- for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:
</del><ins>+ @param calendar: the calendar object to expand
+ @type calendar: L{Component}
+ @param event_details: list to append VEVENT components to
+ @type event_details: C{list}
+ @param tzinfo: timezone for floating time calculations
+ @type tzinfo: L{Timezone}
+ """
</ins><span class="cx">
</span><del>- # Get overall start/end
- start = vav.getStartDateUTC()
- if start is None:
- start = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- end = vav.getEndDateUTC()
- if end is None:
- end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
- period = Period(start, end)
- overall = clipPeriod(period, Period(timerange.start, timerange.end))
- if overall is None:
- continue
</del><ins>+ # First expand the component
+ expanded = calendar.expand(self.timerange.getStart(), self.timerange.getEnd(), timezone=tzinfo)
</ins><span class="cx">
</span><del>- # Now get periods for each instance of AVAILABLE sub-components
- periods = processAvailablePeriods(vav, timerange)
</del><ins>+ keep_props = (
+ "UID",
+ "RECURRENCE-ID",
+ "DTSTAMP",
+ "DTSTART",
+ "DTEND",
+ "DURATION",
+ )
</ins><span class="cx">
</span><del>- # Now invert the periods and store in accumulator
- busyperiods = []
- last_end = timerange.start
- for period in periods:
- if last_end < period.getStart():
- busyperiods.append(Period(last_end, period.getStart()))
- last_end = period.getEnd()
- if last_end < timerange.end:
- busyperiods.append(Period(last_end, timerange.end))
</del><ins>+ if rich_options["organizer"] or rich_options["delegate"]:
+ keep_props += ("SUMMARY",)
</ins><span class="cx">
</span><del>- # Add to actual results mapped by busy type
- fbtype = vav.propertyValue("BUSYTYPE")
- if fbtype is None:
- fbtype = "BUSY-UNAVAILABLE"
</del><ins>+ if rich_options["organizer"] or rich_options["resource"]:
+ keep_props += ("ORGANIZER",)
</ins><span class="cx">
</span><del>- fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
</del><ins>+ # Remove all but essential properties
+ expanded.filterProperties(keep=keep_props)
</ins><span class="cx">
</span><ins>+ # Need to remove all child components of VEVENT
+ for subcomponent in expanded.subcomponents():
+ if subcomponent.name() == "VEVENT":
+ for sub in tuple(subcomponent.subcomponents()):
+ subcomponent.removeComponent(sub)
</ins><span class="cx">
</span><ins>+ self.event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == "VEVENT"])
</ins><span class="cx">
</span><del>-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.
- """
</del><span class="cx">
</span><del>- periods = []
</del><ins>+ def processEventFreeBusy(self, calendar, fbinfo, tzinfo):
+ """
+ Extract free busy data from a VEVENT component.
+ @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
+ @param fbinfo: the tuple used to store the three types of fb data.
+ @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
+ """
</ins><span class="cx">
</span><del>- # First we need to group all AVAILABLE sub-components by UID
- uidmap = {}
- for component in calendar.subcomponents():
- if component.name() == "AVAILABLE":
- uid = component.propertyValue("UID")
- uidmap.setdefault(uid, []).append(component)
</del><ins>+ # Expand out the set of instances for the event with in the required range
+ instances = calendar.expandTimeRanges(self.timerange.getEnd(), lowerLimit=self.timerange.getStart(), ignoreInvalidInstances=True)
</ins><span class="cx">
</span><del>- # Then we expand each uid set separately
- for componentSet in uidmap.itervalues():
- instances = InstanceList(ignoreInvalidInstances=True)
- instances.expandTimeRanges(componentSet, timerange.end)
</del><ins>+ # Can only do timed events
+ for key in instances:
+ instance = instances[key]
+ if instance.start.isDateOnly():
+ return
+ break
+ else:
+ return
</ins><span class="cx">
</span><del>- # Now convert instances into period list
</del><span class="cx"> for key in instances:
</span><span class="cx"> instance = instances[key]
</span><del>- # Ignore any with floating times (which should not happen as the spec requires UTC or local
- # but we will try and be safe here).
- start = instance.start
- if start.floating():
</del><ins>+
+ # Apply a timezone to any floating times
+ fbstart = instance.start
+ if fbstart.floating():
+ fbstart.setTimezone(tzinfo)
+ fbend = instance.end
+ if fbend.floating():
+ fbend.setTimezone(tzinfo)
+
+ # Check TRANSP property of underlying component
+ if instance.component.hasProperty("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":
</ins><span class="cx"> continue
</span><del>- end = instance.end
- if end.floating():
- continue
</del><span class="cx">
</span><span class="cx"> # Clip period for this instance - use duration for period end if that
</span><span class="cx"> # is what original component used
</span><span class="cx"> if instance.component.hasProperty("DURATION"):
</span><del>- period = Period(start, duration=end - start)
</del><ins>+ period = Period(fbstart, duration=fbend - fbstart)
</ins><span class="cx"> else:
</span><del>- period = Period(start, end)
- clipped = clipPeriod(period, Period(timerange.start, timerange.end))
</del><ins>+ period = Period(fbstart, fbend)
+ clipped = clipPeriod(period, self.timerange)
+
+ # Double check for overlap
</ins><span class="cx"> if clipped:
</span><del>- periods.append(clipped)
</del><ins>+ if status == "TENTATIVE":
+ fbinfo.tentative.append(clipped)
+ else:
+ fbinfo.busy.append(clipped)
</ins><span class="cx">
</span><del>- normalizePeriodList(periods)
- return periods
</del><span class="cx">
</span><ins>+ def processFreeBusyFreeBusy(self, calendar, fbinfo):
+ """
+ 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.
+ """
</ins><span class="cx">
</span><ins>+ 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, self.timerange.getStart(), self.timerange.getEnd()):
+ continue
</ins><span class="cx">
</span><del>-def buildFreeBusyResult(fbinfo, timerange, organizer=None, attendee=None, uid=None, method=None, event_details=None):
- """
- Generate a VCALENDAR object containing a single VFREEBUSY that is the
- aggregate of the free busy info passed in.
</del><ins>+ # Now look at each FREEBUSY property
+ for fb in vfb.properties("FREEBUSY"):
+ # Check the type
+ fbtype = fb.parameterValue("FBTYPE", default="BUSY")
+ if fbtype == "FREE":
+ continue
</ins><span class="cx">
</span><del>- @param fbinfo: the array of busy periods to use.
- @param timerange: the L{TimeRange} for the query.
- @param organizer: the L{Property} for the Organizer of the free busy request, or None.
- @param attendee: the L{Property} for the Attendee responding to the free busy request, or None.
- @param uid: the UID value from the free busy request.
- @param method: the METHOD property value to insert.
- @param event_details: VEVENT components to add.
- @return: the L{Component} containing the calendar data.
- """
</del><ins>+ # 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(), self.timerange)
+ if clipped:
+ getattr(fbinfo, self.FBInfo_mapper.get(fbtype, "busy")).append(clipped)
</ins><span class="cx">
</span><del>- # Merge overlapping time ranges in each fb info section
- normalizePeriodList(fbinfo[0])
- normalizePeriodList(fbinfo[1])
- normalizePeriodList(fbinfo[2])
</del><span class="cx">
</span><del>- # Now build a new calendar object with the free busy info we have
- fbcalendar = Component("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", DateTime.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))
</del><ins>+ def buildFreeBusyResult(self, fbinfo, method=None):
+ """
+ Generate a VCALENDAR object containing a single VFREEBUSY that is the
+ aggregate of the free busy info passed in.
</ins><span class="cx">
</span><del>- if event_details:
- for vevent in event_details:
- fbcalendar.addComponent(vevent)
</del><ins>+ @param fbinfo: the array of busy periods to use.
+ @param method: the METHOD property value to insert.
+ @return: the L{Component} containing the calendar data.
+ """
</ins><span class="cx">
</span><del>- return fbcalendar
</del><ins>+ # Merge overlapping time ranges in each fb info section
+ normalizePeriodList(fbinfo.busy)
+ normalizePeriodList(fbinfo.tentative)
+ normalizePeriodList(fbinfo.unavailable)
+
+ # Now build a new calendar object with the free busy info we have
+ fbcalendar = Component("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 self.organizerProp is not None:
+ fb.addProperty(self.organizerProp)
+ if self.attendeeProp is not None:
+ fb.addProperty(self.attendeeProp)
+ fb.addProperty(Property("DTSTART", self.timerange.getStart()))
+ fb.addProperty(Property("DTEND", self.timerange.getEnd()))
+ fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC()))
+ if len(fbinfo.busy) != 0:
+ fb.addProperty(Property("FREEBUSY", fbinfo.busy, {"FBTYPE": "BUSY"}))
+ if len(fbinfo.tentative) != 0:
+ fb.addProperty(Property("FREEBUSY", fbinfo.tentative, {"FBTYPE": "BUSY-TENTATIVE"}))
+ if len(fbinfo.unavailable) != 0:
+ fb.addProperty(Property("FREEBUSY", fbinfo.unavailable, {"FBTYPE": "BUSY-UNAVAILABLE"}))
+ if self.uid is not None:
+ fb.addProperty(Property("UID", self.uid))
+ else:
+ uid = str(uuid.uuid4())
+ fb.addProperty(Property("UID", uid))
+
+ if self.event_details:
+ for vevent in self.event_details:
+ fbcalendar.addComponent(vevent)
+
+ return fbcalendar
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -16,6 +16,7 @@
</span><span class="cx">
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="cx"> from pycalendar.duration import Duration
</span><ins>+from pycalendar.period import Period
</ins><span class="cx"> from pycalendar.timezone import Timezone
</span><span class="cx">
</span><span class="cx"> from twext.python.log import Logger
</span><span class="lines">@@ -23,13 +24,13 @@
</span><span class="cx">
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx">
</span><del>-from twistedcaldav import customxml, caldavxml
</del><ins>+from twistedcaldav import customxml
</ins><span class="cx"> from twistedcaldav.accounting import emitAccounting, accountingEnabled
</span><span class="cx"> from twistedcaldav.config import config
</span><span class="cx"> from twistedcaldav.ical import Property
</span><span class="cx"> from twistedcaldav.instance import InvalidOverriddenInstanceError
</span><span class="cx">
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTipProcessing, iTIPRequestStatus, \
</span><span class="cx"> iTipGenerator
</span><span class="cx"> from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord
</span><span class="lines">@@ -799,6 +800,9 @@
</span><span class="cx"> # in any calendars.
</span><span class="cx"> uid = calendar.resourceUID()
</span><span class="cx">
</span><ins>+ # Object to do freebusy query
+ freebusy = FreebusyQuery(None, None, None, None, None, None, uid, None, accountingItems=accounting if len(instances) == 1 else None)
+
</ins><span class="cx"> # Now compare each instance time-range with the index and see if there is an overlap
</span><span class="cx"> fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
</span><span class="cx"> fbset = [fbcalendar for fbcalendar in fbset if fbcalendar.isUsedForFreeBusy()]
</span><span class="lines">@@ -822,7 +826,7 @@
</span><span class="cx"> if instance.partstat == "NEEDS-ACTION" and instance.free and instance.active:
</span><span class="cx"> try:
</span><span class="cx"> # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
</span><del>- fbinfo = ([], [], [])
</del><ins>+ fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx">
</span><span class="cx"> def makeTimedUTC(dt):
</span><span class="cx"> dt = dt.duplicate()
</span><span class="lines">@@ -834,18 +838,19 @@
</span><span class="cx"> dt.adjustToUTC()
</span><span class="cx"> return dt
</span><span class="cx">
</span><del>- tr = caldavxml.TimeRange(
- start=str(makeTimedUTC(instance.start)),
- end=str(makeTimedUTC(instance.end)),
</del><ins>+ tr = Period(
+ makeTimedUTC(instance.start),
+ makeTimedUTC(instance.end),
</ins><span class="cx"> )
</span><span class="cx">
</span><del>- yield generateFreeBusyInfo(testcal, fbinfo, tr, 0, uid, servertoserver=True, accountingItems=accounting if len(instances) == 1 else None)
</del><ins>+ freebusy.timerange = tr
+ yield freebusy.generateFreeBusyInfo(testcal, fbinfo, 0)
</ins><span class="cx">
</span><span class="cx"> # If any fbinfo entries exist we have an overlap
</span><del>- if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
</del><ins>+ if len(fbinfo.busy) or len(fbinfo.tentative) or len(fbinfo.unavailable):
</ins><span class="cx"> instance.free = False
</span><span class="cx"> if accounting is not None:
</span><del>- accounting["tr"].insert(0, (tr.attributes["start"], tr.attributes["end"], instance.free,))
</del><ins>+ accounting["tr"].insert(0, (tr.getStart().getText(), tr.getEnd().getText(), instance.free,))
</ins><span class="cx"> except QueryMaxResources:
</span><span class="cx"> instance.free[instance] = False
</span><span class="cx"> log.info("Exceeded number of matches whilst trying to find free-time.")
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -42,6 +42,8 @@
</span><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.delivery import ScheduleViaISchedule
</span><span class="cx"> from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
</span><span class="cx">
</span><ins>+from pycalendar.period import Period
+
</ins><span class="cx"> import hashlib
</span><span class="cx"> from collections import namedtuple
</span><span class="cx">
</span><span class="lines">@@ -400,9 +402,7 @@
</span><span class="cx"> "VFREEBUSY start or end not UTC",
</span><span class="cx"> ))
</span><span class="cx">
</span><del>- self.timeRange = caldavxml.TimeRange(start=dtstart.getText(), end=dtend.getText())
- self.timeRange.start = dtstart
- self.timeRange.end = dtend
</del><ins>+ self.timeRange = Period(dtstart, dtend)
</ins><span class="cx">
</span><span class="cx"> # Look for masked UID
</span><span class="cx"> self.excludeUID = self.calendar.getMaskUID()
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_freebusypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -22,11 +22,10 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks
</span><span class="cx"> from twisted.trial.unittest import TestCase
</span><span class="cx">
</span><del>-from twistedcaldav import caldavxml
</del><span class="cx"> from twistedcaldav.ical import Component, Property
</span><span class="cx">
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import buildFreeBusyResult, \
- generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.common.datastore.test.util import CommonCommonTests, populateCalendarsFrom
</span><span class="cx">
</span><span class="cx"> def normalizeiCalendarText(data):
</span><span class="lines">@@ -46,13 +45,8 @@
</span><span class="cx"> data = (
</span><span class="cx"> (
</span><span class="cx"> "#1.1 No busy time",
</span><del>- [
- [],
- [],
- [],
- ],
- "20080601T000000Z",
- "20080602T000000Z",
</del><ins>+ FreebusyQuery.FBInfo([], [], []),
+ Period.parseText("20080601T000000Z/20080602T000000Z"),
</ins><span class="cx"> None,
</span><span class="cx"> None,
</span><span class="cx"> None,
</span><span class="lines">@@ -68,13 +62,8 @@
</span><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "#1.2 No busy time with organizer & attendee",
</span><del>- [
- [],
- [],
- [],
- ],
- "20080601T000000Z",
- "20080602T000000Z",
</del><ins>+ FreebusyQuery.FBInfo([], [], []),
+ Period.parseText("20080601T000000Z/20080602T000000Z"),
</ins><span class="cx"> Property("ORGANIZER", "mailto:user01@example.com"),
</span><span class="cx"> Property("ATTENDEE", "mailto:user02@example.com"),
</span><span class="cx"> None,
</span><span class="lines">@@ -92,13 +81,8 @@
</span><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "#1.3 With single busy time",
</span><del>- [
- [Period.parseText("20080601T120000Z/20080601T130000Z"), ],
- [],
- [],
- ],
- "20080601T000000Z",
- "20080602T000000Z",
</del><ins>+ FreebusyQuery.FBInfo([Period.parseText("20080601T120000Z/20080601T130000Z"), ], [], []),
+ Period.parseText("20080601T000000Z/20080602T000000Z"),
</ins><span class="cx"> Property("ORGANIZER", "mailto:user01@example.com"),
</span><span class="cx"> Property("ATTENDEE", "mailto:user02@example.com"),
</span><span class="cx"> None,
</span><span class="lines">@@ -117,16 +101,15 @@
</span><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "#1.4 With multiple busy time",
</span><del>- [
</del><ins>+ FreebusyQuery.FBInfo(
</ins><span class="cx"> [
</span><span class="cx"> Period.parseText("20080601T120000Z/20080601T130000Z"),
</span><span class="cx"> Period.parseText("20080601T140000Z/20080601T150000Z"),
</span><span class="cx"> ],
</span><span class="cx"> [],
</span><span class="cx"> [],
</span><del>- ],
- "20080601T000000Z",
- "20080602T000000Z",
</del><ins>+ ),
+ Period.parseText("20080601T000000Z/20080602T000000Z"),
</ins><span class="cx"> Property("ORGANIZER", "mailto:user01@example.com"),
</span><span class="cx"> Property("ATTENDEE", "mailto:user02@example.com"),
</span><span class="cx"> None,
</span><span class="lines">@@ -145,7 +128,7 @@
</span><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "#1.5 With multiple busy time, some overlap",
</span><del>- [
</del><ins>+ FreebusyQuery.FBInfo(
</ins><span class="cx"> [
</span><span class="cx"> Period.parseText("20080601T120000Z/20080601T130000Z"),
</span><span class="cx"> Period.parseText("20080601T123000Z/20080601T133000Z"),
</span><span class="lines">@@ -154,9 +137,8 @@
</span><span class="cx"> ],
</span><span class="cx"> [],
</span><span class="cx"> [],
</span><del>- ],
- "20080601T000000Z",
- "20080602T000000Z",
</del><ins>+ ),
+ Period.parseText("20080601T000000Z/20080602T000000Z"),
</ins><span class="cx"> Property("ORGANIZER", "mailto:user01@example.com"),
</span><span class="cx"> Property("ATTENDEE", "mailto:user02@example.com"),
</span><span class="cx"> None,
</span><span class="lines">@@ -175,7 +157,7 @@
</span><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "#1.6 With all busy time types",
</span><del>- [
</del><ins>+ FreebusyQuery.FBInfo(
</ins><span class="cx"> [
</span><span class="cx"> Period.parseText("20080601T120000Z/20080601T130000Z"),
</span><span class="cx"> Period.parseText("20080601T140000Z/20080601T150000Z"),
</span><span class="lines">@@ -186,9 +168,8 @@
</span><span class="cx"> [
</span><span class="cx"> Period.parseText("20080601T160000Z/20080601T170000Z"),
</span><span class="cx"> ],
</span><del>- ],
- "20080601T000000Z",
- "20080602T000000Z",
</del><ins>+ ),
+ Period.parseText("20080601T000000Z/20080602T000000Z"),
</ins><span class="cx"> Property("ORGANIZER", "mailto:user01@example.com"),
</span><span class="cx"> Property("ATTENDEE", "mailto:user02@example.com"),
</span><span class="cx"> None,
</span><span class="lines">@@ -209,13 +190,12 @@
</span><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "#1.7 With single busy time and event details",
</span><del>- [
</del><ins>+ FreebusyQuery.FBInfo(
</ins><span class="cx"> [Period.parseText("20080601T120000Z/20080601T130000Z"), ],
</span><span class="cx"> [],
</span><span class="cx"> [],
</span><del>- ],
- "20080601T000000Z",
- "20080602T000000Z",
</del><ins>+ ),
+ Period.parseText("20080601T000000Z/20080602T000000Z"),
</ins><span class="cx"> Property("ORGANIZER", "mailto:user01@example.com"),
</span><span class="cx"> Property("ATTENDEE", "mailto:user02@example.com"),
</span><span class="cx"> [
</span><span class="lines">@@ -250,16 +230,16 @@
</span><span class="cx"> ),
</span><span class="cx"> )
</span><span class="cx">
</span><del>- for description, fbinfo, dtstart, dtend, organizer, attendee, event_details, calendar in data:
- timerange = caldavxml.TimeRange(start=dtstart, end=dtend)
- result = buildFreeBusyResult(fbinfo, timerange, organizer=organizer, attendee=attendee, event_details=event_details)
</del><ins>+ for description, fbinfo, timerange, organizer, attendee, event_details, calendar in data:
+ freebusy = FreebusyQuery(None, organizer, None, attendee, None, timerange, None, None, event_details=event_details)
+ result = freebusy.buildFreeBusyResult(fbinfo)
</ins><span class="cx"> self.assertEqual(normalizeiCalendarText(str(result)), calendar.replace("\n", "\r\n"), msg=description)
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> class GenerateFreeBusyInfo(CommonCommonTests, TestCase):
</span><span class="cx"> """
</span><del>- Test txdav.caldav.datastore.scheduling.freebusy.generateFreeBusyInfo
</del><ins>+ Test txdav.caldav.datastore.scheduling.freebusy.FreebusyQuery
</ins><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -332,14 +312,16 @@
</span><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
</span><del>- fbinfo = [[], [], [], ]
</del><ins>+ fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx"> matchtotal = 0
</span><del>- timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
- result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
</del><ins>+ timerange = Period(self.now, self.now_1D)
+
+ freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
+ result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
</ins><span class="cx"> self.assertEqual(result, 0)
</span><del>- self.assertEqual(len(fbinfo[0]), 0)
- self.assertEqual(len(fbinfo[1]), 0)
- self.assertEqual(len(fbinfo[2]), 0)
</del><ins>+ self.assertEqual(len(fbinfo.busy), 0)
+ self.assertEqual(len(fbinfo.tentative), 0)
+ self.assertEqual(len(fbinfo.unavailable), 0)
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -362,14 +344,16 @@
</span><span class="cx">
</span><span class="cx"> yield self._createCalendarObject(data, "user01", "test.ics")
</span><span class="cx"> calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
</span><del>- fbinfo = [[], [], [], ]
</del><ins>+ fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx"> matchtotal = 0
</span><del>- timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
- result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
</del><ins>+ timerange = Period(self.now, self.now_1D)
+
+ freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
+ result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
</ins><span class="cx"> self.assertEqual(result, 1)
</span><del>- self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
- self.assertEqual(len(fbinfo[1]), 0)
- self.assertEqual(len(fbinfo[2]), 0)
</del><ins>+ self.assertEqual(fbinfo.busy, [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
+ self.assertEqual(len(fbinfo.tentative), 0)
+ self.assertEqual(len(fbinfo.unavailable), 0)
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -392,21 +376,23 @@
</span><span class="cx">
</span><span class="cx"> yield self._createCalendarObject(data, "user01", "test.ics")
</span><span class="cx"> calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
</span><del>- fbinfo = [[], [], [], ]
</del><ins>+ fbinfo = FreebusyQuery.FBInfo([], [], [])
</ins><span class="cx"> matchtotal = 0
</span><del>- timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
</del><ins>+ timerange = Period(self.now, self.now_1D)
</ins><span class="cx"> event_details = []
</span><del>- result = (yield generateFreeBusyInfo(
</del><ins>+
+ organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:user01@example.com", self.transactionUnderTest()))
+
+ freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None, event_details=event_details)
+ freebusy.same_calendar_user = True
+ result = (yield freebusy.generateFreeBusyInfo(
</ins><span class="cx"> calendar,
</span><span class="cx"> fbinfo,
</span><del>- timerange,
</del><span class="cx"> matchtotal,
</span><del>- organizer="mailto:user01@example.com",
- event_details=event_details
</del><span class="cx"> ))
</span><span class="cx"> self.assertEqual(result, 1)
</span><del>- self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
- self.assertEqual(len(fbinfo[1]), 0)
- self.assertEqual(len(fbinfo[2]), 0)
</del><ins>+ self.assertEqual(fbinfo.busy, [Period(self.now_12H, self.now_13H), ])
+ self.assertEqual(len(fbinfo.tentative), 0)
+ self.assertEqual(len(fbinfo.unavailable), 0)
</ins><span class="cx"> self.assertEqual(len(event_details), 1)
</span><span class="cx"> self.assertEqual(str(event_details[0]), str(tuple(Component.fromString(data).subcomponents())[0]))
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastorepoddingstore_apipy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/podding/store_api.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/podding/store_api.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/common/datastore/podding/store_api.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -16,11 +16,13 @@
</span><span class="cx">
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx">
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.common.datastore.podding.util import UtilityConduitMixin
</span><span class="cx"> from txdav.common.datastore.sql_tables import _HOME_STATUS_DISABLED
</span><span class="cx">
</span><del>-from twistedcaldav.caldavxml import TimeRange
</del><ins>+from pycalendar.period import Period
+
</ins><span class="cx"> from datetime import datetime
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -69,13 +71,11 @@
</span><span class="cx"> def send_freebusy(
</span><span class="cx"> self,
</span><span class="cx"> calresource,
</span><ins>+ organizer,
+ recipient,
</ins><span class="cx"> timerange,
</span><span class="cx"> matchtotal,
</span><span class="cx"> excludeuid,
</span><del>- organizer,
- organizerPrincipal,
- same_calendar_user,
- servertoserver,
</del><span class="cx"> event_details,
</span><span class="cx"> ):
</span><span class="cx"> """
</span><span class="lines">@@ -84,13 +84,11 @@
</span><span class="cx"> """
</span><span class="cx"> txn, request, server = yield self._getRequestForStoreObject("freebusy", calresource, False)
</span><span class="cx">
</span><del>- request["timerange"] = [timerange.start.getText(), timerange.end.getText()]
</del><ins>+ request["organizer"] = organizer
+ request["recipient"] = organizer
+ request["timerange"] = timerange.getText()
</ins><span class="cx"> request["matchtotal"] = matchtotal
</span><span class="cx"> request["excludeuid"] = excludeuid
</span><del>- request["organizer"] = organizer
- request["organizerPrincipal"] = organizerPrincipal
- request["same_calendar_user"] = same_calendar_user
- request["servertoserver"] = servertoserver
</del><span class="cx"> request["event_details"] = event_details
</span><span class="cx">
</span><span class="cx"> response = yield self.sendRequestToServer(txn, server, request)
</span><span class="lines">@@ -109,28 +107,26 @@
</span><span class="cx"> # Operate on the L{CommonHomeChild}
</span><span class="cx"> calresource, _ignore = yield self._getStoreObjectForRequest(txn, request)
</span><span class="cx">
</span><del>- fbinfo = [[], [], []]
- matchtotal = yield generateFreeBusyInfo(
</del><ins>+ organizer = yield calendarUserFromCalendarUserAddress(request["organizer"], txn)
+ recipient = yield calendarUserFromCalendarUserAddress(request["recipient"], txn)
+
+ freebusy = FreebusyQuery(
+ organizer, None, recipient, None, None,
+ Period.parseText(request["timerange"]), request["excludeuid"], None, event_details=request["event_details"])
+ fbinfo = FreebusyQuery.FBInfo([], [], [])
+ matchtotal = yield freebusy.generateFreeBusyInfo(
</ins><span class="cx"> calresource,
</span><span class="cx"> fbinfo,
</span><del>- TimeRange(start=request["timerange"][0], end=request["timerange"][1]),
</del><span class="cx"> request["matchtotal"],
</span><del>- request["excludeuid"],
- request["organizer"],
- request["organizerPrincipal"],
- request["same_calendar_user"],
- request["servertoserver"],
- request["event_details"],
- logItems=None
</del><span class="cx"> )
</span><span class="cx">
</span><del>- # Convert L{DateTime} objects to text for JSON response
- for i in range(3):
- for j in range(len(fbinfo[i])):
- fbinfo[i][j] = fbinfo[i][j].getText()
-
</del><ins>+ # Convert L{Period} objects to text for JSON response
</ins><span class="cx"> returnValue({
</span><del>- "fbresults": fbinfo,
</del><ins>+ "fbresults": [
+ [item.getText() for item in fbinfo.busy],
+ [item.getText() for item in fbinfo.tentative],
+ [item.getText() for item in fbinfo.unavailable],
+ ],
</ins><span class="cx"> "matchtotal": matchtotal,
</span><span class="cx"> })
</span><span class="cx">
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastorepoddingtesttest_conduitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py (14682 => 14683)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py        2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py        2015-04-17 18:11:45 UTC (rev 14683)
</span><span class="lines">@@ -26,11 +26,11 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, succeed, returnValue
</span><span class="cx">
</span><span class="cx"> from twistedcaldav import caldavxml
</span><del>-from twistedcaldav.caldavxml import TimeRange
</del><span class="cx"> from twistedcaldav.ical import Component, normalize_iCalStr
</span><span class="cx">
</span><span class="cx"> from txdav.caldav.datastore.query.filter import Filter
</span><del>-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
</del><ins>+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
</ins><span class="cx"> from txdav.caldav.datastore.scheduling.ischedule.localservers import ServersDB, Server
</span><span class="cx"> from txdav.caldav.datastore.sql import ManagedAttachment, AttachmentLink
</span><span class="cx"> from txdav.caldav.datastore.test.common import CaptureProtocol
</span><span class="lines">@@ -927,21 +927,13 @@
</span><span class="cx">
</span><span class="cx"> shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
</span><span class="cx">
</span><del>- fbinfo = [[], [], []]
- matchtotal = yield generateFreeBusyInfo(
- shared,
- fbinfo,
- TimeRange(start=fbstart, end=fbend),
- 0,
- excludeuid=None,
- organizer=None,
- organizerPrincipal=None,
- same_calendar_user=False,
- servertoserver=False,
- event_details=False,
- logItems=None
- )
</del><ins>+ fbinfo = FreebusyQuery.FBInfo([], [], [])
+ timerange = Period(DateTime.parseText(fbstart), DateTime.parseText(fbend))
+ organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:puser01@example.com", self.theTransactionUnderTest(1)))
</ins><span class="cx">
</span><ins>+ freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None)
+ matchtotal = (yield freebusy.generateFreeBusyInfo(shared, fbinfo, 0))
+
</ins><span class="cx"> self.assertEqual(matchtotal, 1)
</span><span class="cx"> self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
</span><span class="cx"> self.assertEqual(len(fbinfo[1]), 0)
</span></span></pre>
</div>
</div>
</body>
</html>