[prev in list] [next in list] [prev in thread] [next in thread]
List: calendarserver-changes
Subject: [CalendarServer-changes] [3295] CalendarServer/trunk/twistedcaldav
From: source_changes () macosforge ! org
Date: 2008-10-31 2:33:19
Message-ID: 20081031023319.ADB2B5F7A38 () beta ! macosforge ! org
[Download RAW message or body]
[Attachment #2 (multipart/alternative)]
Revision: 3295
http://trac.macosforge.org/projects/calendarserver/changeset/3295
Author: cdaboo@apple.com
Date: 2008-10-30 19:33:19 -0700 (Thu, 30 Oct 2008)
Log Message:
-----------
Major refactor to implement proper PUT/COPY/MOVE/DELETE logic with implicit \
scheduling and also to restrict duplicate UIDs for scheduling messages.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/customxml.py
CalendarServer/trunk/twistedcaldav/method/delete.py
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py 2008-10-31 02:20:00 UTC (rev \
3294)
+++ CalendarServer/trunk/twistedcaldav/customxml.py 2008-10-31 02:33:19 UTC (rev \
3295) @@ -60,6 +60,14 @@
def getValue(self):
return str(self)
+class TwistedSchedulingObjectResource (davxml.WebDAVEmptyElement):
+ """
+ Indicates that the resource is a scheduling object resource.
+ """
+ namespace = twisted_private_namespace
+ name = "scheduling-object-resource"
+ hidden = True
+
class TwistedCalendarHasPrivateCommentsProperty (davxml.WebDAVEmptyElement):
"""
Indicates that a calendar resource has private comments.
Modified: CalendarServer/trunk/twistedcaldav/method/delete.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/delete.py 2008-10-31 02:20:00 UTC (rev \
3294)
+++ CalendarServer/trunk/twistedcaldav/method/delete.py 2008-10-31 02:33:19 UTC (rev \
3295) @@ -22,13 +22,18 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.fileop import delete
from twisted.web2.dav.util import parentForURL
from twisted.web2.http import HTTPError, StatusResponse
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
from twistedcaldav.resource import isCalendarCollectionResource
from twistedcaldav.scheduling.implicit import ImplicitScheduler
+from twistedcaldav.log import Logger
+log = Logger()
+
@inlineCallbacks
def http_DELETE(self, request):
#
@@ -39,10 +44,28 @@
# TODO: need to use transaction based delete on live scheduling object resources
# as the iTIP operation may fail and may need to prevent the delete from \
happening.
+ if not self.fp.exists():
+ log.err("File not found: %s" % (self.fp.path,))
+ raise HTTPError(responsecode.NOT_FOUND)
+
+ depth = request.headers.getHeader("depth", "infinity")
+
+ #
+ # Check authentication and access controls
+ #
parentURL = parentForURL(request.uri)
parent = (yield request.locateResource(parentURL))
- calendar = None
+ yield parent.authorize(request, (davxml.Unbind(),))
+
+ # Do quota checks before we start deleting things
+ myquota = (yield self.quota(request))
+ if myquota is not None:
+ old_size = (yield self.quotaSize(request))
+ else:
+ old_size = 0
+
+ scheduler = None
isCalendarCollection = False
isCalendarResource = False
lock = None
@@ -51,7 +74,12 @@
if isCalendarCollectionResource(parent):
isCalendarResource = True
calendar = self.iCalendar()
- lock = MemcacheLock("ImplicitUIDLock", calendar.resourceUID(), \
timeout=60.0) + scheduler = ImplicitScheduler()
+ do_implicit_action, _ignore = (yield \
scheduler.testImplicitSchedulingDELETE(request, self, calendar)) + if \
do_implicit_action: + lock = MemcacheLock("ImplicitUIDLock", \
calendar.resourceUID(), timeout=60.0) + else:
+ scheduler = None
elif isCalendarCollectionResource(self):
isCalendarCollection = True
@@ -60,8 +88,14 @@
if lock:
yield lock.acquire()
- response = (yield super(CalDAVFile, self).http_DELETE(request))
+ # Do delete
+ response = (yield delete(request.uri, self.fp, depth))
+
+ # Adjust quota
+ if myquota is not None:
+ yield self.quotaSizeAdjust(request, -old_size)
+
if response == responsecode.NO_CONTENT:
if isCalendarResource:
@@ -72,8 +106,8 @@
yield parent.updateCTag()
# Do scheduling
- scheduler = ImplicitScheduler()
- yield scheduler.doImplicitScheduling(request, self, calendar, True)
+ if scheduler:
+ yield scheduler.doImplicitScheduling()
elif isCalendarCollection:
Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-10-31 02:20:00 UTC \
(rev 3294)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-10-31 02:33:19 UTC \
(rev 3295) @@ -25,7 +25,7 @@
from twisted.internet import reactor
from twisted.internet.defer import Deferred, inlineCallbacks, succeed
-from twisted.internet.defer import maybeDeferred, returnValue
+from twisted.internet.defer import returnValue
from twisted.python import failure
from twisted.python.filepath import FilePath
from twisted.web2 import responsecode
@@ -48,9 +48,9 @@
from twistedcaldav.caldavxml import NumberOfRecurrencesWithinLimits
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.customxml import calendarserver_namespace ,\
- TwistedCalendarHasPrivateCommentsProperty
+ TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource
from twistedcaldav.customxml import TwistedCalendarAccessProperty
-from twistedcaldav.fileops import copyToWithXAttrs
+from twistedcaldav.fileops import copyToWithXAttrs, copyXAttrs
from twistedcaldav.fileops import putWithXAttrs
from twistedcaldav.fileops import copyWithXAttrs
from twistedcaldav.ical import Component, Property
@@ -265,11 +265,15 @@
self.rollback = None
self.access = None
+ @inlineCallbacks
def fullValidation(self):
"""
Do full validation of source and destination calendar data.
"""
+ # Basic validation
+ yield self.validCopyMoveOperation()
+
if self.destinationcal:
# Valid resource name check
result, message = self.validResourceName()
@@ -330,10 +334,38 @@
# Check access
if self.destinationcal and config.EnablePrivateEvents:
- return self.validAccess()
+ result = (yield self.validAccess())
+ returnValue(result)
else:
- return succeed(None)
+ returnValue(None)
+
+ elif self.sourcecal:
+ self.source_index = self.sourceparent.index()
+ self.calendar = self.source.iCalendar()
+ @inlineCallbacks
+ def validCopyMoveOperation(self):
+ """
+ Check that copy/move type behavior is valid.
+ """
+ if self.source:
+ if not self.destinationcal:
+ # Don't care about copies/moves to non-calendar destinations
+ # In theory this state should not occur here as COPY/MOVE won't call \
into this as + # they detect this state and do regular WebDAV \
copy/move. + pass
+ elif not self.sourcecal:
+ # Moving into a calendar requires regular checks
+ pass
+ else:
+ # Calendar to calendar moves are OK if the owner is the same
+ sourceowner = (yield self.sourceparent.owner(self.request))
+ destowner = (yield self.destinationparent.owner(self.request))
+ if sourceowner != destowner:
+ msg = "Calendar-to-calendar %s with different owners are not \
supported" % ("moves" if self.deletesource else "copies",) + \
log.debug(msg) + raise \
HTTPError(StatusResponse(responsecode.FORBIDDEN, msg)) +
def validResourceName(self):
"""
Make sure that the resource name for the new resource is valid.
@@ -546,29 +578,99 @@
copyToWithXAttrs(self.source.fp, self.rollback.source_copy)
log.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, \
self.rollback.source_copy.path))
+ def preservePrivateComments(self):
+ # Check for private comments on the old resource and the new resource and \
re-insert + # ones that are lost.
+ #
+ # NB Do this before implicit scheduling as we don't want old clients to \
trigger scheduling when + # the X- property is missing.
+ new_has_private_comments = False
+ if config.Scheduling["CalDAV"].get("EnablePrivateComments", True) and \
self.calendar is not None: + old_has_private_comments = \
self.destination.exists() and self.destinationcal and \
self.destination.hasDeadProperty(TwistedCalendarHasPrivateCommentsProperty) + \
new_has_private_comments = self.calendar.hasPropertyInAnyComponent(( + \
"X-CALENDARSERVER-PRIVATE-COMMENT", + \
"X-CALENDARSERVER-ATTENDEE-COMMENT", + ))
+
+ if old_has_private_comments and not new_has_private_comments:
+ # Transfer old comments to new calendar
+ log.debug("Private Comments properties were entirely removed by the \
client. Restoring existing properties.") + old_calendar = \
self.destination.iCalendar() + \
self.calendar.transferProperties(old_calendar, ( + \
"X-CALENDARSERVER-PRIVATE-COMMENT", + \
"X-CALENDARSERVER-ATTENDEE-COMMENT", + ))
+ self.calendardata = None
+
+ return new_has_private_comments
+
@inlineCallbacks
- def doStore(self):
- # Do put or copy based on whether source exists
- if self.source is not None:
- response = maybeDeferred(copyWithXAttrs, self.source.fp, \
self.destination.fp, self.destination_uri) + def doImplicitScheduling(self):
+ data_changed = False
+
+ # Do scheduling
+ if not self.isiTIP:
+ scheduler = ImplicitScheduler()
+
+ # Determine type of operation PUT, COPY or DELETE
+ if not self.source:
+ # PUT
+ do_implicit_action, is_scheduling_resource = (yield \
scheduler.testImplicitSchedulingPUT( + self.request,
+ self.destination,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+ elif self.deletesource:
+ # MOVE
+ do_implicit_action, is_scheduling_resource = (yield \
scheduler.testImplicitSchedulingMOVE( + self.request,
+ self.source,
+ self.sourcecal,
+ self.source_uri,
+ self.destination,
+ self.destinationcal,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+ else:
+ # COPY
+ do_implicit_action, is_scheduling_resource = (yield \
scheduler.testImplicitSchedulingCOPY( + self.request,
+ self.source,
+ self.sourcecal,
+ self.source_uri,
+ self.destination,
+ self.destinationcal,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+
+ if do_implicit_action and self.allowImplicitSchedule:
+ new_calendar = (yield scheduler.doImplicitScheduling())
+ if new_calendar:
+ self.calendar = new_calendar
+ self.calendardata = str(self.calendar)
+ data_changed = True
else:
- if self.calendardata is None:
- self.calendardata = str(self.calendar)
- md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
- response = maybeDeferred(putWithXAttrs, md5, self.destination.fp)
- response = (yield response)
+ is_scheduling_resource = False
+
+ returnValue((is_scheduling_resource, data_changed,))
- # Update the MD5 value on the resource
+ @inlineCallbacks
+ def doStore(self, implicit):
+ # Do put or copy based on whether source exists
if self.source is not None:
- # Copy MD5 value from source to destination
- if self.source.hasDeadProperty(TwistedGETContentMD5):
- md5 = self.source.readDeadProperty(TwistedGETContentMD5)
- self.destination.writeDeadProperty(md5)
+ if implicit:
+ response = (yield self.doStorePut())
+ copyXAttrs(self.source.fp, self.destination.fp)
+ else:
+ response = (yield copyWithXAttrs(self.source.fp, \
self.destination.fp, self.destination_uri)) else:
- # Finish MD5 calculation and write dead property
- md5.close()
- md5 = md5.getMD5()
- self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+ response = (yield self.doStorePut())
# Update calendar-access property value on the resource
if self.access:
@@ -583,6 +685,21 @@
returnValue(IResponse(response))
@inlineCallbacks
+ def doStorePut(self):
+
+ if self.calendardata is None:
+ self.calendardata = str(self.calendar)
+ md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
+ response = (yield putWithXAttrs(md5, self.destination.fp))
+
+ # Finish MD5 calculation and write dead property
+ md5.close()
+ md5 = md5.getMD5()
+ self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+
+ returnValue(response)
+
+ @inlineCallbacks
def doSourceDelete(self):
# Delete index for original item
if self.sourcecal:
@@ -715,35 +832,11 @@
# Get current quota state.
yield self.checkQuota()
- # Check for private comments on the old resource and the new resource \
and re-insert
- # ones that are lost.
- #
- # NB Do this before implicit scheduling as we don't want old clients to \
trigger scheduling when
- # the X- property is missing.
- if config.Scheduling["CalDAV"].get("EnablePrivateComments", True):
- old_has_private_comments = self.destination.exists() and \
self.destinationcal and \
self.destination.hasDeadProperty(TwistedCalendarHasPrivateCommentsProperty)
- new_has_private_comments = self.calendar.hasPropertyInAnyComponent((
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
-
- if old_has_private_comments and not new_has_private_comments:
- # Transfer old comments to new calendar
- log.debug("Private Comments properties were entirely removed by \
the client. Restoring existing properties.")
- old_calendar = self.destination.iCalendar()
- self.calendar.transferProperties(old_calendar, (
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
- self.calendardata = None
+ # Preserve private comments
+ new_has_private_comments = self.preservePrivateComments()
# Do scheduling
- if not self.isiTIP and self.allowImplicitSchedule:
- scheduler = ImplicitScheduler()
- new_calendar = (yield scheduler.doImplicitScheduling(self.request, \
self.destination, self.calendar, False, \
internal_request=self.internal_request))
- if new_calendar:
- self.calendar = new_calendar
- self.calendardata = str(self.calendar)
+ is_scheduling_resource, data_changed = (yield \
self.doImplicitScheduling())
# Initialize the rollback system
self.setupRollback()
@@ -763,9 +856,15 @@
"""
# Do the actual put or copy
- response = (yield self.doStore())
+ response = (yield self.doStore(data_changed))
+ # Check for scheduling object resource and write property
+ if is_scheduling_resource:
+ self.destination.writeDeadProperty(TwistedSchedulingObjectResource())
+ elif not self.destinationcal:
+ self.destination.removeDeadProperty(TwistedSchedulingObjectResource) \
+
# Check for existence of private comments and write property
if config.Scheduling["CalDAV"].get("EnablePrivateComments", True):
if new_has_private_comments:
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-31 02:20:00 UTC \
(rev 3294)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-31 02:33:19 UTC \
(rev 3295) @@ -17,7 +17,10 @@
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
from twisted.web2 import responsecode
from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.util import parentForURL
from twisted.web2.http import HTTPError
+
from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.scheduling.itip import iTipGenerator
from twistedcaldav.log import Logger
@@ -29,6 +32,7 @@
from twistedcaldav.scheduling import addressmapping
from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
LocalCalendarUser
+from twistedcaldav.customxml import TwistedSchedulingObjectResource
__all__ = [
"ImplicitScheduler",
@@ -51,48 +55,154 @@
pass
@inlineCallbacks
- def doImplicitScheduling(self, request, resource, calendar, deleting, \
internal_request=False):
- """
- Do implicit scheduling operation based on the calendar data that is being \
PUT + def testImplicitSchedulingPUT(self, request, resource, resource_uri, \
calendar, internal_request=False): +
+ self.request = request
+ self.resource = resource
+ self.calendar = calendar
+ self.internal_request = internal_request
- @param request:
- @type request:
- @param resource:
- @type resource:
- @param calendar: the calendar data being written, or None if deleting
- @type calendar: L{Component} or C{None}
- @param deleting: C{True} if the resource is being deleting
- @type deleting: bool
+ existing_resource = resource.exists()
+ existing_type = "schedule" if existing_resource and \
resource.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar" + \
new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
- @return: a new calendar object modified with scheduling information,
- or C{None} if nothing happened
- """
+ if existing_type == "calendar":
+ self.action = "create" if new_type == "schedule" else "none"
+ else:
+ self.action = "modify" if new_type == "schedule" else "remove"
+
+ # Cannot create new resource with existing UID
+ if not existing_resource or self.action == "create":
+ yield self.hasCalendarResourceUIDSomewhereElse(None, resource_uri, \
new_type) +
+ # If action is remove we actually need to get state from the existing \
scheduling object resource + if self.action == "remove":
+ # Also make sure that we return the new calendar being be written rather \
than the old one + # when the implicit action is executed
+ self.return_calendar = calendar
+ self.calendar = resource.iCalendar()
+ yield self.checkImplicitState()
+ # Attendees are not allowed to overwrite one type with another
+ if self.state == "attendee" and (existing_type != new_type) and \
existing_resource: + raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, \
(caldav_namespace, "valid-attendee-change"))) +
+ returnValue((self.action != "none", new_type == "schedule",))
+
+ @inlineCallbacks
+ def testImplicitSchedulingMOVE(self, request, srcresource, srccal, src_uri, \
destresource, destcal, dest_uri, calendar, internal_request=False): +
self.request = request
+ self.resource = destresource
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
+
+ dest_exists = destresource.exists()
+ dest_is_implicit = \
destresource.hasDeadProperty(TwistedSchedulingObjectResource()) if dest_exists else \
False + src_is_implicit = \
srcresource.hasDeadProperty(TwistedSchedulingObjectResource()) or new_type == \
"schedule" +
+ if srccal and destcal:
+ if src_is_implicit and dest_exists or dest_is_implicit:
+ log.debug("Implicit - cannot MOVE with a scheduling object \
resource") + raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, \
(caldav_namespace, "unique-scheduling-object-resource"))) + else:
+ self.action = "none"
+ elif srccal and not destcal:
+ result = (yield self.testImplicitSchedulingDELETE(request, srcresource, \
calendar)) + returnValue((result[0], new_type == "schedule",))
+ elif not srccal and destcal:
+ result = (yield self.testImplicitSchedulingPUT(request, destresource, \
dest_uri, calendar)) + returnValue(result)
+ else:
+ self.action = "none"
+
+ returnValue((self.action != "none", new_type == "schedule",))
+
+ @inlineCallbacks
+ def testImplicitSchedulingCOPY(self, request, srcresource, srccal, src_uri, \
destresource, destcal, dest_uri, calendar, internal_request=False): +
+ self.request = request
+ self.resource = destresource
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
+
+ dest_exists = destresource.exists()
+ dest_is_implicit = \
destresource.hasDeadProperty(TwistedSchedulingObjectResource()) if dest_exists else \
False + src_is_implicit = \
srcresource.hasDeadProperty(TwistedSchedulingObjectResource()) or new_type == \
"schedule" +
+ if srccal and destcal:
+ if src_is_implicit or dest_is_implicit:
+ log.debug("Implicit - cannot COPY with a scheduling object \
resource") + raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, \
(caldav_namespace, "unique-scheduling-object-resource"))) + else:
+ self.action = "none"
+ elif srccal and not destcal:
+ self.action = "none"
+ elif not srccal and destcal:
+ result = (yield self.testImplicitSchedulingPUT(request, destresource, \
dest_uri, calendar)) + returnValue(result)
+ else:
+ self.action = "none"
+
+ returnValue((self.action != "none", src_is_implicit,))
+
+ @inlineCallbacks
+ def testImplicitSchedulingDELETE(self, request, resource, calendar, \
internal_request=False): +
+ self.request = request
self.resource = resource
self.calendar = calendar
- self.calendar_owner = (yield self.resource.owner(self.request))
- self.deleting = deleting
self.internal_request = internal_request
- self.except_attendees = ()
- # When deleting we MUST have the calendar as the actual resource
- # will have been deleted by now
- assert deleting and calendar or not deleting
+ yield self.checkImplicitState()
+ resource_type = "schedule" if \
resource.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar" + \
self.action = "remove" if resource_type == "schedule" else "none" +
+ returnValue((self.action != "none", False,))
+
+ @inlineCallbacks
+ def checkImplicitState(self):
# Get some useful information from the calendar
yield self.extractCalendarData()
+ self.calendar_owner = (yield self.resource.owner(self.request))
# Determine what type of scheduling this is: Organizer triggered or Attendee \
triggered organizer_scheduling = (yield self.isOrganizerScheduling())
if organizer_scheduling:
+ self.state = "organizer"
+ elif self.isAttendeeScheduling():
+ self.state = "attendee"
+ else:
+ self.state = None
+
+ returnValue(self.state is not None)
+
+ @inlineCallbacks
+ def doImplicitScheduling(self):
+ """
+ Do implicit scheduling operation based on the data already set by call to \
checkImplicitScheduling. +
+ @return: a new calendar object modified with scheduling information,
+ or C{None} if nothing happened
+ """
+
+ # Setup some parameters
+ self.except_attendees = ()
+
+ # Determine what type of scheduling this is: Organizer triggered or Attendee \
triggered + if self.state == "organizer":
yield self.doImplicitOrganizer()
- elif self.isAttendeeScheduling():
+ elif self.state == "attendee":
yield self.doImplicitAttendee()
else:
returnValue(None)
- returnValue(self.calendar)
+ returnValue(self.return_calendar if hasattr(self, "return_calendar") else \
self.calendar)
@inlineCallbacks
def refreshAllAttendeesExceptSome(self, request, resource, calendar, attendees):
@@ -109,8 +219,10 @@
self.request = request
self.resource = resource
self.calendar = calendar
+ self.state = "organizer"
+ self.action = "modify"
+
self.calendar_owner = None
- self.deleting = False
self.internal_request = True
self.except_attendees = attendees
self.changed_rids = None
@@ -134,8 +246,10 @@
self.request = request
self.resource = resource
self.calendar = calendar
+ self.action = "modify"
+ self.state = "attendee"
+
self.calendar_owner = None
- self.deleting = False
self.internal_request = True
self.changed_rids = None
@@ -191,6 +305,53 @@
self.uid = self.calendar.resourceUID()
@inlineCallbacks
+ def hasCalendarResourceUIDSomewhereElse(self, src_uri, dest_uri, type):
+ """
+ See if a calendar component with a matching UID exists anywhere in the \
calendar home of the + current recipient owner and is not the resource being \
targeted. + """
+
+ # Don't care in some cases
+ if self.internal_request or self.action == "remove":
+ returnValue(None)
+
+ # Get owner's calendar-home
+ calendar_owner_principal = (yield \
self.resource.ownerPrincipal(self.request)) + calendar_home = \
calendar_owner_principal.calendarHome() +
+ source_parent_uri = parentForURL(src_uri)[:-1] if src_uri else None
+ destination_parent_uri = parentForURL(dest_uri)[:-1] if dest_uri else None
+
+ # FIXME: because of the URL->resource request mapping thing, we have to \
force the request + # to recognize this resource
+ self.request._rememberResource(calendar_home, calendar_home.url())
+
+ # Run a UID query against the UID
+
+ @inlineCallbacks
+ def queryCalendarCollection(collection, uri):
+ rname = collection.index().resourceNameForUID(self.uid)
+ if rname:
+ child = (yield self.request.locateResource(joinURL(uri, rname)))
+ matched_type = "schedule" if child and \
child.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar" + \
if ( + uri != destination_parent_uri and
+ (source_parent_uri is None or uri != source_parent_uri) and
+ (type == "schedule" or matched_type == "schedule")
+ ):
+ log.debug("Implicit - found component with same UID in a \
different collection: %s" % (uri,)) + raise \
HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, \
"unique-scheduling-object-resource"))) +
+ # Here we can always return true as the unique UID in a calendar \
collection + # requirement will already have been tested.
+
+ returnValue(True)
+
+ # NB We are by-passing privilege checking here. That should be OK as the \
data found is not + # exposed to the user.
+ yield report_common.applyToCalendarCollections(calendar_home, self.request, \
calendar_home.url(), "infinity", queryCalendarCollection, None) +
+ @inlineCallbacks
def isOrganizerScheduling(self):
"""
Test whether this is a scheduling operation by an organizer
@@ -253,16 +414,16 @@
yield self.doAccessControl(self.organizerPrincipal, True)
# Check for a delete
- if self.deleting:
+ if self.action == "remove":
- log.debug("Implicit - organizer '%s' is deleting UID: '%s'" % \
(self.organizer, self.uid)) + log.debug("Implicit - organizer '%s' is \
removing UID: '%s'" % (self.organizer, self.uid)) self.oldcalendar = self.calendar
# Cancel all attendees
self.cancelledAttendees = [(attendee, None) for attendee in \
self.attendees]
# Check for a new resource or an update
- elif self.resource.exists():
+ elif self.action == "modify":
# Read in existing data
self.oldcalendar = self.resource.iCalendar()
@@ -271,14 +432,15 @@
no_change, self.changed_rids = self.isChangeInsignificant()
if no_change:
# Nothing to do
- log.debug("Implicit - organizer '%s' is updating UID: '%s' but \
change is not significant" % (self.organizer, self.uid)) + \
log.debug("Implicit - organizer '%s' is modifying UID: '%s' but change is not \
significant" % (self.organizer, self.uid)) returnValue(None)
- log.debug("Implicit - organizer '%s' is updating UID: '%s'" % \
(self.organizer, self.uid)) + log.debug("Implicit - organizer '%s' is \
modifying UID: '%s'" % (self.organizer, self.uid))
# Check for removed attendees
self.findRemovedAttendees()
- else:
+
+ elif self.action == "create":
log.debug("Implicit - organizer '%s' is creating UID: '%s'" % \
(self.organizer, self.uid)) self.oldcalendar = None
self.changed_rids = None
@@ -377,7 +539,7 @@
yield self.processCancels()
# Process regular requests next
- if not self.deleting:
+ if self.action in ("create", "modify",):
yield self.processRequests()
@inlineCallbacks
@@ -402,7 +564,7 @@
if None in rids:
# One big CANCEL will do
- itipmsg = iTipGenerator.generateCancel(self.oldcalendar, \
(attendee,), None, self.deleting) + itipmsg = \
iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None, self.action == \
"remove") else:
# Multiple CANCELs
itipmsg = iTipGenerator.generateCancel(self.oldcalendar, \
(attendee,), rids) @@ -470,9 +632,7 @@
if not self.internal_request:
yield self.doAccessControl(self.attendeePrincipal, False)
- if self.deleting:
- #log.error("Attendee '%s' is not allowed to delete an organized event: \
UID:%s" % (self.attendeePrincipal, self.uid,))
- #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, \
(caldav_namespace, "valid-attendee-change"))) + if self.action == "remove":
log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % \
(self.attendee, self.uid)) yield self.scheduleCancelWithOrganizer()
Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-10-31 \
02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-10-31 \
02:33:19 UTC (rev 3295) @@ -13,8 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from twistedcaldav.memcachelock import MemcacheLock
-from twistedcaldav.memcacher import Memcacher
import os
@@ -26,6 +24,9 @@
from twisted.web2.test.test_server import SimpleRequest
from twistedcaldav.ical import Component
+from twistedcaldav.memcachelock import MemcacheLock
+from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
import twistedcaldav.test.util
class CollectionContents (twistedcaldav.test.util.TestCase):
@@ -35,6 +36,8 @@
data_dir = os.path.join(os.path.dirname(__file__), "data")
def setUp(self):
+
+ # Need to fake out memcache
def _getFakeMemcacheProtocol(self):
result = super(MemcacheLock, self)._getMemcacheProtocol()
@@ -45,6 +48,12 @@
MemcacheLock._getMemcacheProtocol = _getFakeMemcacheProtocol
+ # Need to not do implicit behavior during these tests
+ def _fakeDoImplicitScheduling(self):
+ return False, False
+
+ StoreCalendarObjectResource.doImplicitScheduling = _fakeDoImplicitScheduling
+
super(CollectionContents, self).setUp()
def test_collection_in_calendar(self):
[Attachment #5 (text/html)]
<!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>[3295] CalendarServer/trunk/twistedcaldav</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.macosforge.org/projects/calendarserver/changeset/3295">3295</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2008-10-30 19:33:19 -0700 (Thu, 30 Oct 2008)</dd>
</dl>
<h3>Log Message</h3>
<pre>Major refactor to implement proper PUT/COPY/MOVE/DELETE logic with implicit \
scheduling and also to restrict duplicate UIDs for scheduling messages.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunktwistedcaldavcustomxmlpy">CalendarServer/trunk/twistedcaldav/customxml.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethoddeletepy">CalendarServer/trunk/twistedcaldav/method/delete.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavmethodput_commonpy">CalendarServer/trunk/twistedcaldav/method/put_common.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavschedulingimplicitpy">CalendarServer/trunk/twistedcaldav/scheduling/implicit.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavtesttest_collectioncontentspy">CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunktwistedcaldavcustomxmlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/customxml.py \
(3294 => 3295)</h4> <pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/customxml.py 2008-10-31 \
02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/customxml.py 2008-10-31 02:33:19 UTC (rev \
3295) </span><span class="lines">@@ -60,6 +60,14 @@
</span><span class="cx"> def getValue(self):
</span><span class="cx"> return str(self)
</span><span class="cx">
</span><ins>+class TwistedSchedulingObjectResource (davxml.WebDAVEmptyElement):
+ """
+ Indicates that the resource is a scheduling object resource.
+ """
+ namespace = twisted_private_namespace
+ name = "scheduling-object-resource"
+ hidden = True
+
</ins><span class="cx"> class TwistedCalendarHasPrivateCommentsProperty \
(davxml.WebDAVEmptyElement): </span><span class="cx"> """
</span><span class="cx"> Indicates that a calendar resource has private comments.
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethoddeletepy"></a>
<div class="modfile"><h4>Modified: \
CalendarServer/trunk/twistedcaldav/method/delete.py (3294 => 3295)</h4> <pre \
class="diff"><span> <span class="info">--- \
CalendarServer/trunk/twistedcaldav/method/delete.py 2008-10-31 02:20:00 UTC (rev \
3294)
+++ CalendarServer/trunk/twistedcaldav/method/delete.py 2008-10-31 02:33:19 UTC (rev \
3295) </span><span class="lines">@@ -22,13 +22,18 @@
</span><span class="cx">
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, \
returnValue </span><span class="cx"> from twisted.web2 import responsecode
</span><ins>+from twisted.web2.dav import davxml
+from twisted.web2.dav.fileop import delete
</ins><span class="cx"> from twisted.web2.dav.util import parentForURL
</span><span class="cx"> from twisted.web2.http import HTTPError, StatusResponse
</span><span class="cx">
</span><span class="cx"> from twistedcaldav.memcachelock import MemcacheLock, \
MemcacheLockTimeoutError </span><span class="cx"> from twistedcaldav.resource import \
isCalendarCollectionResource </span><span class="cx"> from \
twistedcaldav.scheduling.implicit import ImplicitScheduler </span><ins>+from \
twistedcaldav.log import Logger </ins><span class="cx">
</span><ins>+log = Logger()
+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def http_DELETE(self, request):
</span><span class="cx"> #
</span><span class="lines">@@ -39,10 +44,28 @@
</span><span class="cx"> # TODO: need to use transaction based delete on live \
scheduling object resources </span><span class="cx"> # as the iTIP operation may \
fail and may need to prevent the delete from happening. </span><span class="cx">
</span><ins>+ if not self.fp.exists():
+ log.err("File not found: %s" % (self.fp.path,))
+ raise HTTPError(responsecode.NOT_FOUND)
+
+ depth = request.headers.getHeader("depth", "infinity")
+
+ #
+ # Check authentication and access controls
+ #
</ins><span class="cx"> parentURL = parentForURL(request.uri)
</span><span class="cx"> parent = (yield request.locateResource(parentURL))
</span><span class="cx">
</span><del>- calendar = None
</del><ins>+ yield parent.authorize(request, (davxml.Unbind(),))
+
+ # Do quota checks before we start deleting things
+ myquota = (yield self.quota(request))
+ if myquota is not None:
+ old_size = (yield self.quotaSize(request))
+ else:
+ old_size = 0
+
+ scheduler = None
</ins><span class="cx"> isCalendarCollection = False
</span><span class="cx"> isCalendarResource = False
</span><span class="cx"> lock = None
</span><span class="lines">@@ -51,7 +74,12 @@
</span><span class="cx"> if isCalendarCollectionResource(parent):
</span><span class="cx"> isCalendarResource = True
</span><span class="cx"> calendar = self.iCalendar()
</span><del>- lock = MemcacheLock("ImplicitUIDLock", \
calendar.resourceUID(), timeout=60.0) </del><ins>+ scheduler = \
ImplicitScheduler() + do_implicit_action, _ignore = (yield \
scheduler.testImplicitSchedulingDELETE(request, self, calendar)) + if \
do_implicit_action: + lock = MemcacheLock("ImplicitUIDLock", \
calendar.resourceUID(), timeout=60.0) + else:
+ scheduler = None
</ins><span class="cx">
</span><span class="cx"> elif isCalendarCollectionResource(self):
</span><span class="cx"> isCalendarCollection = True
</span><span class="lines">@@ -60,8 +88,14 @@
</span><span class="cx"> if lock:
</span><span class="cx"> yield lock.acquire()
</span><span class="cx">
</span><del>- response = (yield super(CalDAVFile, self).http_DELETE(request))
</del><ins>+ # Do delete
+ response = (yield delete(request.uri, self.fp, depth))
</ins><span class="cx">
</span><ins>+
+ # Adjust quota
+ if myquota is not None:
+ yield self.quotaSizeAdjust(request, -old_size)
+
</ins><span class="cx"> if response == responsecode.NO_CONTENT:
</span><span class="cx"> if isCalendarResource:
</span><span class="cx">
</span><span class="lines">@@ -72,8 +106,8 @@
</span><span class="cx"> yield parent.updateCTag()
</span><span class="cx">
</span><span class="cx"> # Do scheduling
</span><del>- scheduler = ImplicitScheduler()
- yield scheduler.doImplicitScheduling(request, self, calendar, True)
</del><ins>+ if scheduler:
+ yield scheduler.doImplicitScheduling()
</ins><span class="cx">
</span><span class="cx"> elif isCalendarCollection:
</span><span class="cx">
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavmethodput_commonpy"></a>
<div class="modfile"><h4>Modified: \
CalendarServer/trunk/twistedcaldav/method/put_common.py (3294 => 3295)</h4> <pre \
class="diff"><span> <span class="info">--- \
CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-10-31 02:20:00 UTC (rev \
3294)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-10-31 02:33:19 UTC \
(rev 3295) </span><span class="lines">@@ -25,7 +25,7 @@
</span><span class="cx">
</span><span class="cx"> from twisted.internet import reactor
</span><span class="cx"> from twisted.internet.defer import Deferred, \
inlineCallbacks, succeed </span><del>-from twisted.internet.defer import \
maybeDeferred, returnValue </del><ins>+from twisted.internet.defer import returnValue
</ins><span class="cx"> from twisted.python import failure
</span><span class="cx"> from twisted.python.filepath import FilePath
</span><span class="cx"> from twisted.web2 import responsecode
</span><span class="lines">@@ -48,9 +48,9 @@
</span><span class="cx"> from twistedcaldav.caldavxml import \
NumberOfRecurrencesWithinLimits </span><span class="cx"> from twistedcaldav.caldavxml \
import caldav_namespace </span><span class="cx"> from twistedcaldav.customxml import \
calendarserver_namespace ,\ </span><del>- \
TwistedCalendarHasPrivateCommentsProperty </del><ins>+ \
TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource \
</ins><span class="cx"> from twistedcaldav.customxml import \
TwistedCalendarAccessProperty </span><del>-from twistedcaldav.fileops import \
copyToWithXAttrs </del><ins>+from twistedcaldav.fileops import copyToWithXAttrs, \
copyXAttrs </ins><span class="cx"> from twistedcaldav.fileops import putWithXAttrs
</span><span class="cx"> from twistedcaldav.fileops import copyWithXAttrs
</span><span class="cx"> from twistedcaldav.ical import Component, Property
</span><span class="lines">@@ -265,11 +265,15 @@
</span><span class="cx"> self.rollback = None
</span><span class="cx"> self.access = None
</span><span class="cx">
</span><ins>+ @inlineCallbacks
</ins><span class="cx"> def fullValidation(self):
</span><span class="cx"> """
</span><span class="cx"> Do full validation of source and destination \
calendar data. </span><span class="cx"> """
</span><span class="cx">
</span><ins>+ # Basic validation
+ yield self.validCopyMoveOperation()
+
</ins><span class="cx"> if self.destinationcal:
</span><span class="cx"> # Valid resource name check
</span><span class="cx"> result, message = self.validResourceName()
</span><span class="lines">@@ -330,10 +334,38 @@
</span><span class="cx">
</span><span class="cx"> # Check access
</span><span class="cx"> if self.destinationcal and \
config.EnablePrivateEvents: </span><del>- return self.validAccess()
</del><ins>+ result = (yield self.validAccess())
+ returnValue(result)
</ins><span class="cx"> else:
</span><del>- return succeed(None)
</del><ins>+ returnValue(None)
+
+ elif self.sourcecal:
+ self.source_index = self.sourceparent.index()
+ self.calendar = self.source.iCalendar()
</ins><span class="cx">
</span><ins>+ @inlineCallbacks
+ def validCopyMoveOperation(self):
+ """
+ Check that copy/move type behavior is valid.
+ """
+ if self.source:
+ if not self.destinationcal:
+ # Don't care about copies/moves to non-calendar destinations
+ # In theory this state should not occur here as COPY/MOVE won't call \
into this as + # they detect this state and do regular WebDAV \
copy/move. + pass
+ elif not self.sourcecal:
+ # Moving into a calendar requires regular checks
+ pass
+ else:
+ # Calendar to calendar moves are OK if the owner is the same
+ sourceowner = (yield self.sourceparent.owner(self.request))
+ destowner = (yield self.destinationparent.owner(self.request))
+ if sourceowner != destowner:
+ msg = "Calendar-to-calendar %s with different owners are \
not supported" % ("moves" if self.deletesource else \
"copies",) + log.debug(msg)
+ raise HTTPError(StatusResponse(responsecode.FORBIDDEN, msg))
+
</ins><span class="cx"> def validResourceName(self):
</span><span class="cx"> """
</span><span class="cx"> Make sure that the resource name for the new \
resource is valid. </span><span class="lines">@@ -546,29 +578,99 @@
</span><span class="cx"> copyToWithXAttrs(self.source.fp, \
self.rollback.source_copy) </span><span class="cx"> \
log.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, \
self.rollback.source_copy.path)) </span><span class="cx">
</span><ins>+ def preservePrivateComments(self):
+ # Check for private comments on the old resource and the new resource and \
re-insert + # ones that are lost.
+ #
+ # NB Do this before implicit scheduling as we don't want old clients to \
trigger scheduling when + # the X- property is missing.
+ new_has_private_comments = False
+ if config.Scheduling["CalDAV"].get("EnablePrivateComments", \
True) and self.calendar is not None: + old_has_private_comments = \
self.destination.exists() and self.destinationcal and \
self.destination.hasDeadProperty(TwistedCalendarHasPrivateCommentsProperty) + \
new_has_private_comments = self.calendar.hasPropertyInAnyComponent(( + \
"X-CALENDARSERVER-PRIVATE-COMMENT", + \
"X-CALENDARSERVER-ATTENDEE-COMMENT", + ))
+
+ if old_has_private_comments and not new_has_private_comments:
+ # Transfer old comments to new calendar
+ log.debug("Private Comments properties were entirely removed by \
the client. Restoring existing properties.") + old_calendar = \
self.destination.iCalendar() + \
self.calendar.transferProperties(old_calendar, ( + \
"X-CALENDARSERVER-PRIVATE-COMMENT", + \
"X-CALENDARSERVER-ATTENDEE-COMMENT", + ))
+ self.calendardata = None
+
+ return new_has_private_comments
+
</ins><span class="cx"> @inlineCallbacks
</span><del>- def doStore(self):
- # Do put or copy based on whether source exists
- if self.source is not None:
- response = maybeDeferred(copyWithXAttrs, self.source.fp, \
self.destination.fp, self.destination_uri) </del><ins>+ def \
doImplicitScheduling(self): + data_changed = False
+
+ # Do scheduling
+ if not self.isiTIP:
+ scheduler = ImplicitScheduler()
+
+ # Determine type of operation PUT, COPY or DELETE
+ if not self.source:
+ # PUT
+ do_implicit_action, is_scheduling_resource = (yield \
scheduler.testImplicitSchedulingPUT( + self.request,
+ self.destination,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+ elif self.deletesource:
+ # MOVE
+ do_implicit_action, is_scheduling_resource = (yield \
scheduler.testImplicitSchedulingMOVE( + self.request,
+ self.source,
+ self.sourcecal,
+ self.source_uri,
+ self.destination,
+ self.destinationcal,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+ else:
+ # COPY
+ do_implicit_action, is_scheduling_resource = (yield \
scheduler.testImplicitSchedulingCOPY( + self.request,
+ self.source,
+ self.sourcecal,
+ self.source_uri,
+ self.destination,
+ self.destinationcal,
+ self.destination_uri,
+ self.calendar,
+ internal_request=self.internal_request,
+ ))
+
+ if do_implicit_action and self.allowImplicitSchedule:
+ new_calendar = (yield scheduler.doImplicitScheduling())
+ if new_calendar:
+ self.calendar = new_calendar
+ self.calendardata = str(self.calendar)
+ data_changed = True
</ins><span class="cx"> else:
</span><del>- if self.calendardata is None:
- self.calendardata = str(self.calendar)
- md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
- response = maybeDeferred(putWithXAttrs, md5, self.destination.fp)
- response = (yield response)
</del><ins>+ is_scheduling_resource = False
+
+ returnValue((is_scheduling_resource, data_changed,))
</ins><span class="cx">
</span><del>- # Update the MD5 value on the resource
</del><ins>+ @inlineCallbacks
+ def doStore(self, implicit):
+ # Do put or copy based on whether source exists
</ins><span class="cx"> if self.source is not None:
</span><del>- # Copy MD5 value from source to destination
- if self.source.hasDeadProperty(TwistedGETContentMD5):
- md5 = self.source.readDeadProperty(TwistedGETContentMD5)
- self.destination.writeDeadProperty(md5)
</del><ins>+ if implicit:
+ response = (yield self.doStorePut())
+ copyXAttrs(self.source.fp, self.destination.fp)
+ else:
+ response = (yield copyWithXAttrs(self.source.fp, \
self.destination.fp, self.destination_uri)) </ins><span class="cx"> else:
</span><del>- # Finish MD5 calculation and write dead property
- md5.close()
- md5 = md5.getMD5()
- self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
</del><ins>+ response = (yield self.doStorePut())
</ins><span class="cx">
</span><span class="cx"> # Update calendar-access property value on the \
resource </span><span class="cx"> if self.access:
</span><span class="lines">@@ -583,6 +685,21 @@
</span><span class="cx"> returnValue(IResponse(response))
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def doStorePut(self):
+
+ if self.calendardata is None:
+ self.calendardata = str(self.calendar)
+ md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
+ response = (yield putWithXAttrs(md5, self.destination.fp))
+
+ # Finish MD5 calculation and write dead property
+ md5.close()
+ md5 = md5.getMD5()
+ self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+
+ returnValue(response)
+
+ @inlineCallbacks
</ins><span class="cx"> def doSourceDelete(self):
</span><span class="cx"> # Delete index for original item
</span><span class="cx"> if self.sourcecal:
</span><span class="lines">@@ -715,35 +832,11 @@
</span><span class="cx"> # Get current quota state.
</span><span class="cx"> yield self.checkQuota()
</span><span class="cx">
</span><del>- # Check for private comments on the old resource and the new \
resource and re-insert
- # ones that are lost.
- #
- # NB Do this before implicit scheduling as we don't want old clients to \
trigger scheduling when
- # the X- property is missing.
- if config.Scheduling["CalDAV"].get("EnablePrivateComments", \
True):
- old_has_private_comments = self.destination.exists() and \
self.destinationcal and \
self.destination.hasDeadProperty(TwistedCalendarHasPrivateCommentsProperty)
- new_has_private_comments = self.calendar.hasPropertyInAnyComponent((
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
-
- if old_has_private_comments and not new_has_private_comments:
- # Transfer old comments to new calendar
- log.debug("Private Comments properties were entirely \
removed by the client. Restoring existing properties.")
- old_calendar = self.destination.iCalendar()
- self.calendar.transferProperties(old_calendar, (
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
- self.calendardata = None
</del><ins>+ # Preserve private comments
+ new_has_private_comments = self.preservePrivateComments()
</ins><span class="cx">
</span><span class="cx"> # Do scheduling
</span><del>- if not self.isiTIP and self.allowImplicitSchedule:
- scheduler = ImplicitScheduler()
- new_calendar = (yield scheduler.doImplicitScheduling(self.request, \
self.destination, self.calendar, False, \
internal_request=self.internal_request))
- if new_calendar:
- self.calendar = new_calendar
- self.calendardata = str(self.calendar)
</del><ins>+ is_scheduling_resource, data_changed = (yield \
self.doImplicitScheduling()) </ins><span class="cx">
</span><span class="cx"> # Initialize the rollback system
</span><span class="cx"> self.setupRollback()
</span><span class="lines">@@ -763,9 +856,15 @@
</span><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> # Do the actual put or copy
</span><del>- response = (yield self.doStore())
</del><ins>+ response = (yield self.doStore(data_changed))
</ins><span class="cx">
</span><span class="cx">
</span><ins>+ # Check for scheduling object resource and write property
+ if is_scheduling_resource:
+ self.destination.writeDeadProperty(TwistedSchedulingObjectResource())
+ elif not self.destinationcal:
+ self.destination.removeDeadProperty(TwistedSchedulingObjectResource) \
+
</ins><span class="cx"> # Check for existence of private comments and \
write property </span><span class="cx"> if \
config.Scheduling["CalDAV"].get("EnablePrivateComments", True): \
</span><span class="cx"> if new_has_private_comments: \
</span></span></pre></div> <a \
id="CalendarServertrunktwistedcaldavschedulingimplicitpy"></a> <div \
class="modfile"><h4>Modified: \
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py (3294 => 3295)</h4> <pre \
class="diff"><span> <span class="info">--- \
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-31 02:20:00 UTC \
(rev 3294)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-31 02:33:19 UTC \
(rev 3295) </span><span class="lines">@@ -17,7 +17,10 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, \
returnValue, succeed </span><span class="cx"> from twisted.web2 import responsecode
</span><span class="cx"> from twisted.web2.dav.http import ErrorResponse
</span><ins>+from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.util import parentForURL
</ins><span class="cx"> from twisted.web2.http import HTTPError
</span><ins>+
</ins><span class="cx"> from twistedcaldav.caldavxml import caldav_namespace
</span><span class="cx"> from twistedcaldav.scheduling.itip import iTipGenerator
</span><span class="cx"> from twistedcaldav.log import Logger
</span><span class="lines">@@ -29,6 +32,7 @@
</span><span class="cx"> from twistedcaldav.scheduling import addressmapping
</span><span class="cx"> from twistedcaldav.scheduling.cuaddress import \
InvalidCalendarUser,\ </span><span class="cx"> LocalCalendarUser
</span><ins>+from twistedcaldav.customxml import TwistedSchedulingObjectResource
</ins><span class="cx">
</span><span class="cx"> __all__ = [
</span><span class="cx"> "ImplicitScheduler",
</span><span class="lines">@@ -51,48 +55,154 @@
</span><span class="cx"> pass
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><del>- def doImplicitScheduling(self, request, resource, calendar, \
deleting, internal_request=False):
- """
- Do implicit scheduling operation based on the calendar data that is being \
PUT </del><ins>+ def testImplicitSchedulingPUT(self, request, resource, \
resource_uri, calendar, internal_request=False): +
+ self.request = request
+ self.resource = resource
+ self.calendar = calendar
+ self.internal_request = internal_request
</ins><span class="cx">
</span><del>- @param request:
- @type request:
- @param resource:
- @type resource:
- @param calendar: the calendar data being written, or None if deleting
- @type calendar: L{Component} or C{None}
- @param deleting: C{True} if the resource is being deleting
- @type deleting: bool
</del><ins>+ existing_resource = resource.exists()
+ existing_type = "schedule" if existing_resource and \
resource.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar" \
+ new_type = "schedule" if (yield self.checkImplicitState()) else \
"calendar" </ins><span class="cx">
</span><del>- @return: a new calendar object modified with scheduling \
information,
- or C{None} if nothing happened
- """
</del><ins>+ if existing_type == "calendar":
+ self.action = "create" if new_type == "schedule" \
else "none" + else:
+ self.action = "modify" if new_type == "schedule" \
else "remove" +
+ # Cannot create new resource with existing UID
+ if not existing_resource or self.action == "create":
+ yield self.hasCalendarResourceUIDSomewhereElse(None, resource_uri, \
new_type) +
+ # If action is remove we actually need to get state from the existing \
scheduling object resource + if self.action == "remove":
+ # Also make sure that we return the new calendar being be written rather \
than the old one + # when the implicit action is executed
+ self.return_calendar = calendar
+ self.calendar = resource.iCalendar()
+ yield self.checkImplicitState()
</ins><span class="cx">
</span><ins>+ # Attendees are not allowed to overwrite one type with another
+ if self.state == "attendee" and (existing_type != new_type) and \
existing_resource: + raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, \
(caldav_namespace, "valid-attendee-change"))) +
+ returnValue((self.action != "none", new_type == \
"schedule",)) +
+ @inlineCallbacks
+ def testImplicitSchedulingMOVE(self, request, srcresource, srccal, src_uri, \
destresource, destcal, dest_uri, calendar, internal_request=False): +
</ins><span class="cx"> self.request = request
</span><ins>+ self.resource = destresource
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ new_type = "schedule" if (yield self.checkImplicitState()) else \
"calendar" +
+ dest_exists = destresource.exists()
+ dest_is_implicit = \
destresource.hasDeadProperty(TwistedSchedulingObjectResource()) if dest_exists else \
False + src_is_implicit = \
srcresource.hasDeadProperty(TwistedSchedulingObjectResource()) or new_type == \
"schedule" +
+ if srccal and destcal:
+ if src_is_implicit and dest_exists or dest_is_implicit:
+ log.debug("Implicit - cannot MOVE with a scheduling object \
resource") + raise \
HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, \
"unique-scheduling-object-resource"))) + else:
+ self.action = "none"
+ elif srccal and not destcal:
+ result = (yield self.testImplicitSchedulingDELETE(request, srcresource, \
calendar)) + returnValue((result[0], new_type == "schedule",))
+ elif not srccal and destcal:
+ result = (yield self.testImplicitSchedulingPUT(request, destresource, \
dest_uri, calendar)) + returnValue(result)
+ else:
+ self.action = "none"
+
+ returnValue((self.action != "none", new_type == \
"schedule",)) +
+ @inlineCallbacks
+ def testImplicitSchedulingCOPY(self, request, srcresource, srccal, src_uri, \
destresource, destcal, dest_uri, calendar, internal_request=False): +
+ self.request = request
+ self.resource = destresource
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ new_type = "schedule" if (yield self.checkImplicitState()) else \
"calendar" +
+ dest_exists = destresource.exists()
+ dest_is_implicit = \
destresource.hasDeadProperty(TwistedSchedulingObjectResource()) if dest_exists else \
False + src_is_implicit = \
srcresource.hasDeadProperty(TwistedSchedulingObjectResource()) or new_type == \
"schedule" +
+ if srccal and destcal:
+ if src_is_implicit or dest_is_implicit:
+ log.debug("Implicit - cannot COPY with a scheduling object \
resource") + raise \
HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, \
"unique-scheduling-object-resource"))) + else:
+ self.action = "none"
+ elif srccal and not destcal:
+ self.action = "none"
+ elif not srccal and destcal:
+ result = (yield self.testImplicitSchedulingPUT(request, destresource, \
dest_uri, calendar)) + returnValue(result)
+ else:
+ self.action = "none"
+
+ returnValue((self.action != "none", src_is_implicit,))
+
+ @inlineCallbacks
+ def testImplicitSchedulingDELETE(self, request, resource, calendar, \
internal_request=False): +
+ self.request = request
</ins><span class="cx"> self.resource = resource
</span><span class="cx"> self.calendar = calendar
</span><del>- self.calendar_owner = (yield self.resource.owner(self.request))
- self.deleting = deleting
</del><span class="cx"> self.internal_request = internal_request
</span><del>- self.except_attendees = ()
</del><span class="cx">
</span><del>- # When deleting we MUST have the calendar as the actual resource
- # will have been deleted by now
- assert deleting and calendar or not deleting
</del><ins>+ yield self.checkImplicitState()
</ins><span class="cx">
</span><ins>+ resource_type = "schedule" if \
resource.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar" \
+ self.action = "remove" if resource_type == "schedule" \
else "none" +
+ returnValue((self.action != "none", False,))
+
+ @inlineCallbacks
+ def checkImplicitState(self):
</ins><span class="cx"> # Get some useful information from the calendar
</span><span class="cx"> yield self.extractCalendarData()
</span><ins>+ self.calendar_owner = (yield self.resource.owner(self.request))
</ins><span class="cx">
</span><span class="cx"> # Determine what type of scheduling this is: \
Organizer triggered or Attendee triggered </span><span class="cx"> \
organizer_scheduling = (yield self.isOrganizerScheduling()) </span><span class="cx"> \
if organizer_scheduling: </span><ins>+ self.state = "organizer"
+ elif self.isAttendeeScheduling():
+ self.state = "attendee"
+ else:
+ self.state = None
+
+ returnValue(self.state is not None)
+
+ @inlineCallbacks
+ def doImplicitScheduling(self):
+ """
+ Do implicit scheduling operation based on the data already set by call to \
checkImplicitScheduling. +
+ @return: a new calendar object modified with scheduling information,
+ or C{None} if nothing happened
+ """
+
+ # Setup some parameters
+ self.except_attendees = ()
+
+ # Determine what type of scheduling this is: Organizer triggered or Attendee \
triggered + if self.state == "organizer":
</ins><span class="cx"> yield self.doImplicitOrganizer()
</span><del>- elif self.isAttendeeScheduling():
</del><ins>+ elif self.state == "attendee":
</ins><span class="cx"> yield self.doImplicitAttendee()
</span><span class="cx"> else:
</span><span class="cx"> returnValue(None)
</span><span class="cx">
</span><del>- returnValue(self.calendar)
</del><ins>+ returnValue(self.return_calendar if hasattr(self, \
"return_calendar") else self.calendar) </ins><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def refreshAllAttendeesExceptSome(self, request, \
resource, calendar, attendees): </span><span class="lines">@@ -109,8 +219,10 @@
</span><span class="cx"> self.request = request
</span><span class="cx"> self.resource = resource
</span><span class="cx"> self.calendar = calendar
</span><ins>+ self.state = "organizer"
+ self.action = "modify"
+
</ins><span class="cx"> self.calendar_owner = None
</span><del>- self.deleting = False
</del><span class="cx"> self.internal_request = True
</span><span class="cx"> self.except_attendees = attendees
</span><span class="cx"> self.changed_rids = None
</span><span class="lines">@@ -134,8 +246,10 @@
</span><span class="cx"> self.request = request
</span><span class="cx"> self.resource = resource
</span><span class="cx"> self.calendar = calendar
</span><ins>+ self.action = "modify"
+ self.state = "attendee"
+
</ins><span class="cx"> self.calendar_owner = None
</span><del>- self.deleting = False
</del><span class="cx"> self.internal_request = True
</span><span class="cx"> self.changed_rids = None
</span><span class="cx">
</span><span class="lines">@@ -191,6 +305,53 @@
</span><span class="cx"> self.uid = self.calendar.resourceUID()
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def hasCalendarResourceUIDSomewhereElse(self, src_uri, dest_uri, \
type): + """
+ See if a calendar component with a matching UID exists anywhere in the \
calendar home of the + current recipient owner and is not the resource being \
targeted. + """
+
+ # Don't care in some cases
+ if self.internal_request or self.action == "remove":
+ returnValue(None)
+
+ # Get owner's calendar-home
+ calendar_owner_principal = (yield \
self.resource.ownerPrincipal(self.request)) + calendar_home = \
calendar_owner_principal.calendarHome() +
+ source_parent_uri = parentForURL(src_uri)[:-1] if src_uri else None
+ destination_parent_uri = parentForURL(dest_uri)[:-1] if dest_uri else None
+
+ # FIXME: because of the URL->resource request mapping thing, we have to \
force the request + # to recognize this resource
+ self.request._rememberResource(calendar_home, calendar_home.url())
+
+ # Run a UID query against the UID
+
+ @inlineCallbacks
+ def queryCalendarCollection(collection, uri):
+ rname = collection.index().resourceNameForUID(self.uid)
+ if rname:
+ child = (yield self.request.locateResource(joinURL(uri, rname)))
+ matched_type = "schedule" if child and \
child.hasDeadProperty(TwistedSchedulingObjectResource()) else "calendar" + \
if ( + uri != destination_parent_uri and
+ (source_parent_uri is None or uri != source_parent_uri) and
+ (type == "schedule" or matched_type == \
"schedule") + ):
+ log.debug("Implicit - found component with same UID in a \
different collection: %s" % (uri,)) + raise \
HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, \
"unique-scheduling-object-resource"))) +
+ # Here we can always return true as the unique UID in a calendar \
collection + # requirement will already have been tested.
+
+ returnValue(True)
+
+ # NB We are by-passing privilege checking here. That should be OK as the \
data found is not + # exposed to the user.
+ yield report_common.applyToCalendarCollections(calendar_home, self.request, \
calendar_home.url(), "infinity", queryCalendarCollection, None) +
+ @inlineCallbacks
</ins><span class="cx"> def isOrganizerScheduling(self):
</span><span class="cx"> """
</span><span class="cx"> Test whether this is a scheduling operation by an \
organizer </span><span class="lines">@@ -253,16 +414,16 @@
</span><span class="cx"> yield \
self.doAccessControl(self.organizerPrincipal, True) </span><span class="cx">
</span><span class="cx"> # Check for a delete
</span><del>- if self.deleting:
</del><ins>+ if self.action == "remove":
</ins><span class="cx">
</span><del>- log.debug("Implicit - organizer '%s' is deleting UID: \
'%s'" % (self.organizer, self.uid)) </del><ins>+ \
log.debug("Implicit - organizer '%s' is removing UID: '%s'" % \
(self.organizer, self.uid)) </ins><span class="cx"> self.oldcalendar = \
self.calendar </span><span class="cx">
</span><span class="cx"> # Cancel all attendees
</span><span class="cx"> self.cancelledAttendees = [(attendee, None) for \
attendee in self.attendees] </span><span class="cx">
</span><span class="cx"> # Check for a new resource or an update
</span><del>- elif self.resource.exists():
</del><ins>+ elif self.action == "modify":
</ins><span class="cx">
</span><span class="cx"> # Read in existing data
</span><span class="cx"> self.oldcalendar = self.resource.iCalendar()
</span><span class="lines">@@ -271,14 +432,15 @@
</span><span class="cx"> no_change, self.changed_rids = \
self.isChangeInsignificant() </span><span class="cx"> if no_change:
</span><span class="cx"> # Nothing to do
</span><del>- log.debug("Implicit - organizer '%s' is updating \
UID: '%s' but change is not significant" % (self.organizer, self.uid)) \
</del><ins>+ log.debug("Implicit - organizer '%s' is modifying \
UID: '%s' but change is not significant" % (self.organizer, self.uid)) \
</ins><span class="cx"> returnValue(None) </span><span class="cx"> \
</span><del>- log.debug("Implicit - organizer '%s' is updating UID: \
'%s'" % (self.organizer, self.uid)) </del><ins>+ \
log.debug("Implicit - organizer '%s' is modifying UID: '%s'" % \
(self.organizer, self.uid)) </ins><span class="cx">
</span><span class="cx"> # Check for removed attendees
</span><span class="cx"> self.findRemovedAttendees()
</span><del>- else:
</del><ins>+
+ elif self.action == "create":
</ins><span class="cx"> log.debug("Implicit - organizer '%s' is \
creating UID: '%s'" % (self.organizer, self.uid)) </span><span class="cx"> \
self.oldcalendar = None </span><span class="cx"> self.changed_rids = None
</span><span class="lines">@@ -377,7 +539,7 @@
</span><span class="cx"> yield self.processCancels()
</span><span class="cx">
</span><span class="cx"> # Process regular requests next
</span><del>- if not self.deleting:
</del><ins>+ if self.action in ("create", "modify",):
</ins><span class="cx"> yield self.processRequests()
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -402,7 +564,7 @@
</span><span class="cx">
</span><span class="cx"> if None in rids:
</span><span class="cx"> # One big CANCEL will do
</span><del>- itipmsg = iTipGenerator.generateCancel(self.oldcalendar, \
(attendee,), None, self.deleting) </del><ins>+ itipmsg = \
iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None, self.action == \
"remove") </ins><span class="cx"> else:
</span><span class="cx"> # Multiple CANCELs
</span><span class="cx"> itipmsg = \
iTipGenerator.generateCancel(self.oldcalendar, (attendee,), rids) </span><span \
class="lines">@@ -470,9 +632,7 @@ </span><span class="cx"> if not \
self.internal_request: </span><span class="cx"> yield \
self.doAccessControl(self.attendeePrincipal, False) </span><span class="cx">
</span><del>- if self.deleting:
- #log.error("Attendee '%s' is not allowed to delete an organized \
event: UID:%s" % (self.attendeePrincipal, self.uid,))
- #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, \
(caldav_namespace, "valid-attendee-change"))) </del><ins>+ if \
self.action == "remove": </ins><span class="cx"> \
log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % \
(self.attendee, self.uid)) </span><span class="cx"> yield \
self.scheduleCancelWithOrganizer() </span><span class="cx">
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavtesttest_collectioncontentspy"></a>
<div class="modfile"><h4>Modified: \
CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py (3294 => \
3295)</h4> <pre class="diff"><span>
<span class="info">--- \
CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-10-31 \
02:20:00 UTC (rev 3294)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py 2008-10-31 \
02:33:19 UTC (rev 3295) </span><span class="lines">@@ -13,8 +13,6 @@
</span><span class="cx"> # See the License for the specific language governing \
permissions and </span><span class="cx"> # limitations under the License.
</span><span class="cx"> ##
</span><del>-from twistedcaldav.memcachelock import MemcacheLock
-from twistedcaldav.memcacher import Memcacher
</del><span class="cx">
</span><span class="cx"> import os
</span><span class="cx">
</span><span class="lines">@@ -26,6 +24,9 @@
</span><span class="cx"> from twisted.web2.test.test_server import SimpleRequest
</span><span class="cx">
</span><span class="cx"> from twistedcaldav.ical import Component
</span><ins>+from twistedcaldav.memcachelock import MemcacheLock
+from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
</ins><span class="cx"> import twistedcaldav.test.util
</span><span class="cx">
</span><span class="cx"> class CollectionContents (twistedcaldav.test.util.TestCase):
</span><span class="lines">@@ -35,6 +36,8 @@
</span><span class="cx"> data_dir = os.path.join(os.path.dirname(__file__), \
"data") </span><span class="cx">
</span><span class="cx"> def setUp(self):
</span><ins>+
+ # Need to fake out memcache
</ins><span class="cx"> def _getFakeMemcacheProtocol(self):
</span><span class="cx">
</span><span class="cx"> result = super(MemcacheLock, \
self)._getMemcacheProtocol() </span><span class="lines">@@ -45,6 +48,12 @@
</span><span class="cx">
</span><span class="cx"> MemcacheLock._getMemcacheProtocol = \
_getFakeMemcacheProtocol </span><span class="cx">
</span><ins>+ # Need to not do implicit behavior during these tests
+ def _fakeDoImplicitScheduling(self):
+ return False, False
+
+ StoreCalendarObjectResource.doImplicitScheduling = _fakeDoImplicitScheduling
+
</ins><span class="cx"> super(CollectionContents, self).setUp()
</span><span class="cx">
</span><span class="cx"> def test_collection_in_calendar(self):
</span></span></pre>
</div>
</div>
</body>
</html>
_______________________________________________
calendarserver-changes mailing list
calendarserver-changes@lists.macosforge.org
http://lists.macosforge.org/mailman/listinfo.cgi/calendarserver-changes
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic