[CalendarServer-changes] [10306] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Tue Jan 15 13:15:07 PST 2013
Revision: 10306
http://trac.calendarserver.org//changeset/10306
Author: cdaboo at apple.com
Date: 2013-01-15 13:15:06 -0800 (Tue, 15 Jan 2013)
Log Message:
-----------
Tweak Prefer header syntax for changes in (now approved) draft RFC.
Modified Paths:
--------------
CalendarServer/trunk/twext/web2/dav/method/propfind.py
CalendarServer/trunk/twext/web2/dav/method/proppatch.py
CalendarServer/trunk/twext/web2/http_headers.py
CalendarServer/trunk/twext/web2/test/test_http_headers.py
CalendarServer/trunk/twistedcaldav/method/mkcol.py
CalendarServer/trunk/twistedcaldav/method/post.py
CalendarServer/trunk/twistedcaldav/method/propfind.py
CalendarServer/trunk/twistedcaldav/method/put.py
CalendarServer/trunk/twistedcaldav/method/report_common.py
CalendarServer/trunk/twistedcaldav/storebridge.py
Modified: CalendarServer/trunk/twext/web2/dav/method/propfind.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/method/propfind.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twext/web2/dav/method/propfind.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -8,10 +8,10 @@
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
-#
+#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
-#
+#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -40,7 +40,7 @@
from twext.web2 import responsecode
from twext.web2.http import StatusResponse
from txdav.xml import element as davxml
-from twext.web2.dav.http import MultiStatusResponse, statusForFailure,\
+from twext.web2.dav.http import MultiStatusResponse, statusForFailure, \
ErrorResponse
from twext.web2.dav.util import normalizeURL, davXMLFromStream
@@ -107,15 +107,15 @@
#
request_uri = request.uri
depth = request.headers.getHeader("depth", "infinity")
-
+
# By policy we will never allow a depth:infinity propfind
if depth == "infinity":
raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.PropfindFiniteDepth()))
# Look for Prefer header first, then try Brief
prefer = request.headers.getHeader("prefer", {})
- returnMinimal = "return-minimal" in prefer
- noRoot = "depth-noroot" in prefer
+ returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer])
+ noRoot = any([key == "depth-noroot" and value is None for key, value, _ignore_args in prefer])
if not returnMinimal:
returnMinimal = request.headers.getHeader("brief", False)
@@ -198,11 +198,12 @@
for status in properties_by_status:
properties = properties_by_status[status]
- if not properties: continue
+ if not properties:
+ continue
- xml_status = davxml.Status.fromResponseCode(status)
+ xml_status = davxml.Status.fromResponseCode(status)
xml_container = davxml.PropertyContainer(*properties)
- xml_propstat = davxml.PropertyStatus(xml_container, xml_status)
+ xml_propstat = davxml.PropertyStatus(xml_container, xml_status)
propstats.append(xml_propstat)
Modified: CalendarServer/trunk/twext/web2/dav/method/proppatch.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/method/proppatch.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twext/web2/dav/method/proppatch.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -8,10 +8,10 @@
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
-#
+#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
-#
+#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -86,7 +86,7 @@
# Look for Prefer header
prefer = request.headers.getHeader("prefer", {})
- returnMinimal = "return-minimal" in prefer
+ returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer])
try:
#
@@ -146,7 +146,7 @@
else:
responses.add(responsecode.OK, property)
- # Only add undo action for those that succeed because those that fail will not have changed
+ # Only add undo action for those that succeed because those that fail will not have changed
undoActions.append(undo)
yield True
@@ -196,7 +196,7 @@
responses.error()
#
- # Return response - use 200 if Prefer:return-minimal set and no errors
+ # Return response - use 200 if Prefer:return=minimal set and no errors
#
if returnMinimal and not gotError:
yield responsecode.OK
Modified: CalendarServer/trunk/twext/web2/http_headers.py
===================================================================
--- CalendarServer/trunk/twext/web2/http_headers.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twext/web2/http_headers.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -855,6 +855,13 @@
+def parsePrefer(field):
+ etype, args = parseArgs(field)
+ etype = parseKeyValue(etype)
+ return (etype[0], etype[1], args)
+
+
+
#### Header generators
def generateAccept(accept):
mimeType, q = accept
@@ -1032,6 +1039,18 @@
+def generatePrefer(items):
+ key, value, args = items
+ if value is None:
+ out = '%s' % (key,)
+ else:
+ out = '%s=%s' % (key, value)
+ if args:
+ out += ';' + generateKeyValues(args)
+ return out
+
+
+
####
class ETag(object):
def __init__(self, tag, weak=False):
@@ -1732,7 +1751,7 @@
'If-Range': (parseIfRange,),
'If-Unmodified-Since': (last, parseDateTime),
'Max-Forwards': (last, int),
- 'Prefer': (tokenize, listParser(parseExpect), dict), # Prefer like Expect
+ 'Prefer': (tokenize, listParser(parsePrefer), list),
# 'Proxy-Authorization': str, # what is "credentials"
'Range': (tokenize, parseRange),
'Referer': (last, str), # TODO: URI object?
@@ -1756,7 +1775,7 @@
'If-Range': (generateIfRange, singleHeader),
'If-Unmodified-Since': (generateDateTime, singleHeader),
'Max-Forwards': (str, singleHeader),
- 'Prefer': (iteritems, listGenerator(generateExpect), singleHeader), # Prefer like Expect
+ 'Prefer': (listGenerator(generatePrefer), singleHeader),
# 'Proxy-Authorization': str, # what is "credentials"
'Range': (generateRange, singleHeader),
'Referer': (str, singleHeader),
Modified: CalendarServer/trunk/twext/web2/test/test_http_headers.py
===================================================================
--- CalendarServer/trunk/twext/web2/test/test_http_headers.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twext/web2/test/test_http_headers.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -449,6 +449,18 @@
self.runRoundtripTest("Expect", table)
+ def testPrefer(self):
+ table = (
+ ("wait",
+ [("wait", None, [])]),
+ ("return = representation",
+ [("return", "representation", [])]),
+ ("return =minimal;arg1;arg2=val2",
+ [("return", "minimal", [("arg1", None), ("arg2", "val2")])]),
+ )
+ self.runRoundtripTest("Prefer", table)
+
+
def testFrom(self):
self.runRoundtripTest("From", (("webmaster at w3.org", "webmaster at w3.org"),))
Modified: CalendarServer/trunk/twistedcaldav/method/mkcol.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/mkcol.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twistedcaldav/method/mkcol.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -111,7 +111,7 @@
if doc is not None:
- # Can ignore Prefer:return-minimal as we don't return a body for success by default
+ # Can ignore Prefer:return=minimal as we don't return a body for success by default
# Parse response body
mkcol = doc.root_element
Modified: CalendarServer/trunk/twistedcaldav/method/post.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/post.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twistedcaldav/method/post.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -133,7 +133,7 @@
# Look for Prefer header
prefer = request.headers.getHeader("prefer", {})
- returnRepresentation = "return-representation" in prefer
+ returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
if returnRepresentation and result.code / 100 == 2:
result = (yield newchild.http_GET(request))
@@ -201,7 +201,7 @@
# Look for Prefer header
prefer = request.headers.getHeader("prefer", {})
- returnRepresentation = "return-representation" in prefer
+ returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
if returnRepresentation and result.code / 100 == 2:
result = (yield newchild.http_GET(request))
Modified: CalendarServer/trunk/twistedcaldav/method/propfind.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/propfind.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twistedcaldav/method/propfind.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -115,8 +115,8 @@
# Look for Prefer header first, then try Brief
prefer = request.headers.getHeader("prefer", {})
- returnMinimal = "return-minimal" in prefer
- noRoot = "depth-noroot" in prefer
+ returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer])
+ noRoot = any([key == "depth-noroot" and value is None for key, value, _ignore_args in prefer])
if not returnMinimal:
returnMinimal = request.headers.getHeader("brief", False)
Modified: CalendarServer/trunk/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twistedcaldav/method/put.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -31,7 +31,7 @@
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.method.put_common import StoreCalendarObjectResource
-from twistedcaldav.resource import isPseudoCalendarCollectionResource,\
+from twistedcaldav.resource import isPseudoCalendarCollectionResource, \
CalDAVResource
log = Logger()
@@ -57,7 +57,7 @@
(caldav_namespace, "supported-calendar-data"),
"Invalid MIME type for calendar collection",
))
-
+
# Read the calendar component from the stream
try:
calendardata = (yield allDataFromStream(request.stream))
@@ -75,24 +75,24 @@
))
storer = StoreCalendarObjectResource(
- request = request,
- destination = self,
- destination_uri = request.uri,
- destinationcal = True,
- destinationparent = parent,
- calendar = calendardata,
+ request=request,
+ destination=self,
+ destination_uri=request.uri,
+ destinationcal=True,
+ destinationparent=parent,
+ calendar=calendardata,
)
result = (yield storer.run())
# Look for Prefer header
prefer = request.headers.getHeader("prefer", {})
- returnRepresentation = "return-representation" in prefer
+ returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
if returnRepresentation and result.code / 100 == 2:
oldcode = result.code
result = (yield self.http_GET(request))
if oldcode == responsecode.CREATED:
- result.code = responsecode.CREATED
+ result.code = responsecode.CREATED
result.headers.setHeader("content-location", request.path)
returnValue(result)
@@ -112,7 +112,7 @@
(carddav_namespace, "supported-address-data"),
"Invalid MIME type for address book collection",
))
-
+
# Read the vcard component from the stream
try:
vcarddata = (yield allDataFromStream(request.stream))
@@ -130,25 +130,25 @@
))
storer = StoreAddressObjectResource(
- request = request,
- sourceadbk = False,
- vcard = vcarddata,
- destination = self,
- destination_uri = request.uri,
- destinationadbk = True,
- destinationparent = parent,
+ request=request,
+ sourceadbk=False,
+ vcard=vcarddata,
+ destination=self,
+ destination_uri=request.uri,
+ destinationadbk=True,
+ destinationparent=parent,
)
result = (yield storer.run())
# Look for Prefer header
prefer = request.headers.getHeader("prefer", {})
- returnRepresentation = "return-representation" in prefer
+ returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
if returnRepresentation and result.code / 100 == 2:
oldcode = result.code
result = (yield self.http_GET(request))
if oldcode == responsecode.CREATED:
- result.code = responsecode.CREATED
+ result.code = responsecode.CREATED
result.headers.setHeader("content-location", request.path)
returnValue(result)
@@ -166,5 +166,5 @@
if clength == 0:
clength = self.contentLength()
request.extendedLogItems["cl"] = str(clength)
-
+
returnValue(result)
Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -59,7 +59,7 @@
from twistedcaldav.datafilters.hiddeninstance import HiddenInstanceFilter
from twistedcaldav.datafilters.privateevents import PrivateEventFilter
from twistedcaldav.datafilters.addressdata import AddressDataFilter
-from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap,\
+from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap, \
compareDateTime, normalizeToUTC, parseSQLTimestampToPyCalendar
from twistedcaldav.ical import Component, Property, iCalendarProductID
from twistedcaldav.instance import InstanceList
@@ -76,8 +76,8 @@
log = Logger()
-COLLECTION_TYPE_REGULAR = "collection"
-COLLECTION_TYPE_CALENDAR = "calendar"
+COLLECTION_TYPE_REGULAR = "collection"
+COLLECTION_TYPE_CALENDAR = "calendar"
COLLECTION_TYPE_ADDRESSBOOK = "adressbook"
@inlineCallbacks
@@ -86,7 +86,7 @@
Run an operation on all calendar collections, starting at the specified
root, to the specified depth. This involves scanning the URI hierarchy
down from the root. Return a MultiStatus element of all responses.
-
+
@param request: the L{IRequest} for the current request.
@param resource: the L{CalDAVResource} representing the root to start scanning
for calendar collections.
@@ -110,20 +110,22 @@
resources = [(resource, request_uri)]
else:
resources = []
- yield resource.findCalendarCollections(depth, request, lambda x, y: resources.append((x, y)), privileges = privileges)
-
+ yield resource.findCalendarCollections(depth, request, lambda x, y: resources.append((x, y)), privileges=privileges)
+
for calresource, uri in resources:
result = (yield apply(calresource, uri))
if not result:
break
+
+
@inlineCallbacks
def applyToAddressBookCollections(resource, request, request_uri, depth, apply, privileges):
"""
Run an operation on all address book collections, starting at the specified
root, to the specified depth. This involves scanning the URI hierarchy
down from the root. Return a MultiStatus element of all responses.
-
+
@param request: the L{IRequest} for the current request.
@param resource: the L{CalDAVResource} representing the root to start scanning
for address book collections.
@@ -138,7 +140,7 @@
try:
yield resource.checkPrivileges(request, privileges)
except AccessDeniedError:
- returnValue( None )
+ returnValue(None)
# When scanning we only go down as far as an address book collection - not into one
if resource.isAddressBookCollection():
@@ -147,13 +149,15 @@
resources = [(resource, request_uri)]
else:
resources = []
- yield resource.findAddressBookCollections(depth, request, lambda x, y: resources.append((x, y)), privileges = privileges)
-
+ yield resource.findAddressBookCollections(depth, request, lambda x, y: resources.append((x, y)), privileges=privileges)
+
for addrresource, uri in resources:
result = yield apply(addrresource, uri)
if not result:
break
+
+
def responseForHref(request, responses, href, resource, propertiesForResource, propertyreq, isowner=True, calendar=None, timezone=None, vcard=None):
"""
Create an appropriate property status response for the given resource.
@@ -197,6 +201,8 @@
d.addCallback(_defer)
return d
+
+
def allPropertiesForResource(request, prop, resource, calendar=None, timezone=None, vcard=None, isowner=True):
"""
Return all (non-hidden) properties for the specified resource.
@@ -223,6 +229,8 @@
d.addCallback(_defer)
return d
+
+
def propertyNamesForResource(request, prop, resource, calendar=None, timezone=None, vcard=None, isowner=True): #@UnusedVariable
"""
Return property names for all properties on the specified resource.
@@ -243,11 +251,13 @@
responsecode.OK: [propertyName(p) for p in props]
}
return properties_by_status
-
+
d = resource.listProperties(request)
d.addCallback(_defer)
return d
+
+
def propertyListForResource(request, prop, resource, calendar=None, timezone=None, vcard=None, isowner=True):
"""
Return the specified properties on the specified resource.
@@ -262,9 +272,11 @@
C{False} otherwise.
@return: a map of OK and NOT FOUND property values.
"""
-
+
return _namedPropertiesForResource(request, prop.children, resource, calendar, timezone, vcard, isowner)
+
+
def validPropertyListCalendarDataTypeVersion(prop):
"""
If the supplied prop element includes a calendar-data element, verify that
@@ -274,7 +286,7 @@
@return: a tuple: (True/False if the calendar-data element is one we can handle or not present,
error message).
"""
-
+
result = True
message = ""
generate_calendar_data = False
@@ -282,12 +294,14 @@
if isinstance(property, caldavxml.CalendarData):
if not property.verifyTypeVersion([("text/calendar", "2.0")]):
result = False
- message = "Calendar-data element type/version not supported: content-type: %s, version: %s" % (property.content_type,property.version)
+ message = "Calendar-data element type/version not supported: content-type: %s, version: %s" % (property.content_type, property.version)
generate_calendar_data = True
break
return result, message, generate_calendar_data
+
+
def validPropertyListAddressDataTypeVersion(prop):
"""
If the supplied prop element includes an address-data element, verify that
@@ -297,7 +311,7 @@
@return: a tuple: (True/False if the address-data element is one we can handle or not present,
error message).
"""
-
+
result = True
message = ""
generate_address_data = False
@@ -305,12 +319,14 @@
if isinstance(property, carddavxml.AddressData):
if not property.verifyTypeVersion([("text/vcard", "3.0")]):
result = False
- message = "Address-data element type/version not supported: content-type: %s, version: %s" % (property.content_type,property.version)
+ message = "Address-data element type/version not supported: content-type: %s, version: %s" % (property.content_type, property.version)
generate_address_data = True
break
return result, message, generate_address_data
+
+
@inlineCallbacks
def _namedPropertiesForResource(request, props, resource, calendar=None, timezone=None, vcard=None, isowner=True):
"""
@@ -333,10 +349,10 @@
responsecode.OK : [],
responsecode.NOT_FOUND : [],
}
-
+
# Look for Prefer header first, then try Brief
prefer = request.headers.getHeader("prefer", {})
- returnMinimal = "return-minimal" in prefer
+ returnMinimal = any([key == "return" and value == "minimal" for key, value, _ignore_args in prefer])
if not returnMinimal:
returnMinimal = request.headers.getHeader("brief", False)
@@ -351,7 +367,7 @@
propvalue = CalendarData().fromCalendar(filtered)
properties_by_status[responsecode.OK].append(propvalue)
continue
-
+
if isinstance(property, carddavxml.AddressData):
if vcard is None:
vcard = (yield resource.vCard())
@@ -359,12 +375,12 @@
propvalue = AddressData().fromAddress(filtered)
properties_by_status[responsecode.OK].append(propvalue)
continue
-
+
if isinstance(property, element.WebDAVElement):
qname = property.qname()
else:
qname = property
-
+
has = (yield resource.hasProperty(property, request))
if has:
@@ -377,12 +393,13 @@
except HTTPError:
f = Failure()
status = statusForFailure(f, "getting property: %s" % (qname,))
- if status not in properties_by_status: properties_by_status[status] = []
+ if status not in properties_by_status:
+ properties_by_status[status] = []
if not returnMinimal or status != responsecode.NOT_FOUND:
properties_by_status[status].append(propertyName(qname))
elif not returnMinimal:
properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
-
+
returnValue(properties_by_status)
fbtype_mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
@@ -391,38 +408,40 @@
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 + PyCalendarDuration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
cached_end = entry.timerange.end - PyCalendarDuration(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)
+ returnValue(None)
+
+
@classmethod
@inlineCallbacks
def makeCacheEntry(cls, calresource, useruid, timerange, fbresults):
@@ -432,6 +451,8 @@
entry = cls(key, token, timerange, fbresults)
yield fbcacher.set(key, entry)
+
+
@inlineCallbacks
def generateFreeBusyInfo(
request,
@@ -479,7 +500,7 @@
organizer_principal = calresource.principalForCalendarUserAddress(organizer) if organizer else None
organizer_uid = organizer_principal.principalUID() if organizer_principal else ""
- # Free busy is per-user
+ # Free busy is per-user
userPrincipal = (yield calresource.resourceOwnerPrincipal(request))
if userPrincipal:
useruid = userPrincipal.principalUID()
@@ -501,7 +522,7 @@
}
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
@@ -518,12 +539,11 @@
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
@@ -532,22 +552,22 @@
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(PyCalendarDateTime.getToday() + PyCalendarDuration(days=-config.FreeBusyCacheDaysBack))
+ cache_start = normalizeToUTC(PyCalendarDateTime.getToday() + PyCalendarDuration(days=0 - config.FreeBusyCacheDaysBack))
cache_end = normalizeToUTC(PyCalendarDateTime.getToday() + PyCalendarDuration(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(
+ filter = caldavxml.Filter(
caldavxml.ComponentFilter(
caldavxml.ComponentFilter(
cache_timerange if caching else timerange,
@@ -558,7 +578,7 @@
)
filter = calendarqueryfilter.Filter(filter)
tzinfo = filter.settimezone(tz)
-
+
try:
resources = yield maybeDeferred(calresource.index().indexedSearch,
filter, useruid=useruid, fbtype=True
@@ -567,7 +587,7 @@
yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources)
except IndexedSearchException:
resources = yield maybeDeferred(calresource.index().bruteForceSearch)
-
+
else:
# Log extended item
if not hasattr(request, "extendedLogItems"):
@@ -592,13 +612,13 @@
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
@@ -612,7 +632,7 @@
# 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':
@@ -624,9 +644,9 @@
fbend.setTimezone(tzinfo)
else:
fbend.setTimezone(PyCalendarTimezone(utc=True))
-
+
# Clip instance to time range
- clipped = clipPeriod(PyCalendarPeriod(fbstart, duration=fbend-fbstart), PyCalendarPeriod(timerange.start, timerange.end))
+ clipped = clipPeriod(PyCalendarPeriod(fbstart, duration=fbend - fbstart), PyCalendarPeriod(timerange.start, timerange.end))
# Double check for overlap
if clipped:
@@ -638,7 +658,7 @@
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))
@@ -648,14 +668,14 @@
else:
child = (yield request.locateChildResource(calresource, name))
calendar = (yield child.iCalendarForUser(request))
-
+
# The calendar may come back as None if the resource is being changed, or was deleted
# between our initial index query and getting here. For now we will ignore this error, but in
# the longer term we need to implement some form of locking, perhaps.
if calendar is None:
log.err("Calendar %s is missing from calendar collection %r" % (name, calresource))
continue
-
+
# Ignore ones of this UID
if excludeuid:
# See if we have a UID match
@@ -663,20 +683,20 @@
test_organizer = calendar.getOrganizer()
test_principal = 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":
@@ -691,14 +711,16 @@
child = (yield request.locateChildResource(calresource, name))
calendar = (yield child.iCalendarForUser(request))
_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
@@ -739,6 +761,7 @@
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.
@@ -747,10 +770,10 @@
@param timerange: the time range to restrict free busy data to.
@param tzinfo: the L{PyCalendarTimezone} for the timezone to use for floating/all-day events.
"""
-
+
# Expand out the set of instances for the event with in the required range
instances = calendar.expandTimeRanges(timerange.end, lowerLimit=timerange.start, ignoreInvalidInstances=True)
-
+
# Can only do timed events
for key in instances:
instance = instances[key]
@@ -759,7 +782,7 @@
break
else:
return
-
+
for key in instances:
instance = instances[key]
@@ -770,19 +793,19 @@
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
@@ -790,11 +813,11 @@
# Clip period for this instance - use duration for period end if that
# is what original component used
if instance.component.hasProperty("DURATION"):
- period = PyCalendarPeriod(fbstart, duration=fbend-fbstart)
+ period = PyCalendarPeriod(fbstart, duration=fbend - fbstart)
else:
period = PyCalendarPeriod(fbstart, fbend)
clipped = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))
-
+
# Double check for overlap
if clipped:
if status == "TENTATIVE":
@@ -802,6 +825,8 @@
else:
fbinfo[0].append(clipped)
+
+
def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
"""
Extract FREEBUSY data from a VFREEBUSY component.
@@ -817,14 +842,14 @@
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():
@@ -833,6 +858,8 @@
if clipped:
fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
+
+
def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
"""
Extract free-busy data from a VAVAILABILITY component.
@@ -840,7 +867,7 @@
@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
@@ -854,10 +881,10 @@
overall = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))
if overall is None:
continue
-
+
# Now get periods for each instance of AVAILABLE sub-components
periods = processAvailablePeriods(vav, timerange)
-
+
# Now invert the periods and store in accumulator
busyperiods = []
last_end = timerange.start
@@ -874,15 +901,16 @@
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
@@ -891,12 +919,12 @@
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]
@@ -912,16 +940,18 @@
# Clip period for this instance - use duration for period end if that
# is what original component used
if instance.component.hasProperty("DURATION"):
- period = PyCalendarPeriod(start, duration=end-start)
+ period = PyCalendarPeriod(start, duration=end - start)
else:
period = PyCalendarPeriod(start, end)
clipped = clipPeriod(period, PyCalendarPeriod(timerange.start, timerange.end))
if clipped:
periods.append(clipped)
-
+
normalizePeriodList(periods)
return periods
+
+
def buildFreeBusyResult(fbinfo, timerange, organizer=None, attendee=None, uid=None, method=None, event_details=None):
"""
Generate a VCALENDAR object containing a single VFREEBUSY that is the
@@ -936,12 +966,12 @@
@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"))
Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py 2013-01-15 18:55:31 UTC (rev 10305)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py 2013-01-15 21:15:06 UTC (rev 10306)
@@ -2774,7 +2774,9 @@
result = (yield storer.run())
# Look for Prefer header
- if "return-representation" in request.headers.getHeader("prefer", {}) and result.code / 100 == 2:
+ prefer = request.headers.getHeader("prefer", {})
+ returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
+ if returnRepresentation and result.code / 100 == 2:
result = (yield self.render(request))
result.code = OK
result.headers.setHeader("content-location", request.path)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130115/5b9a34ee/attachment-0001.html>
More information about the calendarserver-changes
mailing list