[CalendarServer-changes] [10122] CalendarServer/branches/users/cdaboo/managed-attachments

source_changes at macosforge.org source_changes at macosforge.org
Tue Dec 4 08:48:49 PST 2012


Revision: 10122
          http://trac.calendarserver.org//changeset/10122
Author:   cdaboo at apple.com
Date:     2012-12-04 08:48:48 -0800 (Tue, 04 Dec 2012)
Log Message:
-----------
Support managed attachments with sharing. Support MOVE with managed attachments.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py
    CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_addressbook_common.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -45,18 +45,18 @@
 log = Logger()
 
 class StoreAddressObjectResource(object):
-    
+
     class UIDReservation(object):
-        
+
         def __init__(self, index, uid, uri):
             self.reserved = False
             self.index = index
             self.uid = uid
             self.uri = uri
-            
+
         @inlineCallbacks
         def reserve(self):
-            
+
             # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
             # time to whoever has the reservation to finish and release it.
             failure_count = 0
@@ -68,34 +68,35 @@
                 except ReservationError:
                     self.reserved = False
                 failure_count += 1
-                
+
                 pause = Deferred()
                 def _timedDeferred():
                     pause.callback(True)
                 reactor.callLater(0.5, _timedDeferred) #@UndefinedVariable
                 yield pause
-            
+
             if self.uri and not self.reserved:
                 raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (self.uri,)))
-        
+
         @inlineCallbacks
         def unreserve(self):
             if self.reserved:
                 yield self.index.unreserveUID(self.uid)
                 self.reserved = False
 
+
     def __init__(
         self,
         request,
         source=None, source_uri=None, sourceparent=None, sourceadbk=False, deletesource=False,
         destination=None, destination_uri=None, destinationparent=None, destinationadbk=True,
         vcard=None,
-        indexdestination = True,
+        indexdestination=True,
         returnData=False,
    ):
         """
         Function that does common PUT/COPY/MOVE behavior.
-        
+
         @param request:           the L{twext.web2.server.Request} for the current HTTP request.
         @param source:            the L{CalDAVResource} for the source resource to copy from, or None if source data
             is to be read from the request.
@@ -110,7 +111,7 @@
         @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
         @param returnData:         True if the caller wants the actual data written to the store returned
         """
-        
+
         # Check that all arguments are valid
         try:
             assert destination is not None and destinationparent is not None and destination_uri is not None
@@ -131,7 +132,7 @@
             log.err("vcard=%s\n" % (vcard,))
             log.err("deletesource=%s\n" % (deletesource,))
             raise
-    
+
         self.request = request
         self.sourceadbk = sourceadbk
         self.destinationadbk = destinationadbk
@@ -146,7 +147,7 @@
         self.deletesource = deletesource
         self.indexdestination = indexdestination
         self.returnData = returnData
-        
+
         self.access = None
 
 
@@ -161,7 +162,7 @@
             result, message = self.validResourceName()
             if not result:
                 log.err(message)
-                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
 
             # Valid collection size check on the destination parent resource
             result, message = (yield self.validCollectionSize())
@@ -182,9 +183,9 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (carddav_namespace, "supported-address-data"),
-                            "Invalid content-type",
+                            message,
                         ))
-                
+
                     # At this point we need the calendar data to do more tests
                     self.vcard = (yield self.source.vCard())
                 else:
@@ -198,7 +199,7 @@
                             (carddav_namespace, "valid-address-data"),
                             "Could not parse vCard",
                         ))
-                        
+
                 # Valid vcard data check
                 result, message = self.validAddressDataCheck()
                 if not result:
@@ -206,9 +207,9 @@
                     raise HTTPError(ErrorResponse(
                         responsecode.FORBIDDEN,
                         (carddav_namespace, "valid-address-data"),
-                        description=message
+                        message
                     ))
-                    
+
                 # Valid vcard data for CalDAV check
                 result, message = self.validCardDAVDataCheck()
                 if not result:
@@ -216,7 +217,7 @@
                     raise HTTPError(ErrorResponse(
                         responsecode.FORBIDDEN,
                         (carddav_namespace, "valid-addressbook-object-resource"),
-                        "Invalid vCard data",
+                        message,
                     ))
 
                 # Must have a valid UID at this point
@@ -244,12 +245,13 @@
                 raise HTTPError(ErrorResponse(
                     responsecode.FORBIDDEN,
                     (carddav_namespace, "max-resource-size"),
-                    "Address data too large",
+                    message,
                 ))
 
             # Check access
             returnValue(None)
-    
+
+
     def validResourceName(self):
         """
         Make sure that the resource name for the new resource is valid.
@@ -259,10 +261,11 @@
         filename = self.destination.name()
         if filename.startswith("."):
             result = False
-            message = "File name %s not allowed in vcard collection" % (filename,)
+            message = "Resource name %s not allowed in vcard collection" % (filename,)
 
         return result, message
-        
+
+
     def validContentType(self):
         """
         Make sure that the content-type of the source resource is text/vcard.
@@ -276,7 +279,8 @@
             message = "MIME type %s not allowed in vcard collection" % (content_type,)
 
         return result, message
-        
+
+
     @inlineCallbacks
     def validCollectionSize(self):
         """
@@ -291,7 +295,8 @@
                 message = "Too many resources in collection %s" % (self.destinationparent,)
 
         returnValue((result, message,))
-        
+
+
     def validAddressDataCheck(self):
         """
         Check that the calendar data is valid iCalendar.
@@ -309,9 +314,10 @@
             except ValueError, e:
                 result = False
                 message = "Invalid vcard data: %s" % (e,)
-        
+
         return result, message
-    
+
+
     def validCardDAVDataCheck(self):
         """
         Check that the vcard data is valid vCard.
@@ -325,9 +331,10 @@
         except ValueError, e:
             result = False
             message = "vCard data does not conform to CardDAV requirements: %s" % (e,)
-        
+
         return result, message
-    
+
+
     def validSizeCheck(self):
         """
         Make sure that the content-type of the source resource is text/vcard.
@@ -373,7 +380,7 @@
             # the other PUT tries to reserve and fails but no index entry exists yet.
             if rname is None:
                 rname = "<<Unknown Resource>>"
-            
+
             result = False
             message = "Address book resource %s already exists with same UID %s" % (rname, uid)
         else:
@@ -384,7 +391,7 @@
                     rname = self.destination.name()
                     result = False
                     message = "Cannot overwrite vcard resource %s with different UID %s" % (rname, olduid)
-        
+
         returnValue((result, message, rname))
 
 
@@ -408,9 +415,10 @@
             self.destination.newStoreProperties().update(sourceProperties)
         else:
             response = (yield self.doStorePut())
-    
+
         returnValue(response)
 
+
     @inlineCallbacks
     def doStorePut(self):
 
@@ -418,6 +426,7 @@
         response = (yield self.destination.storeStream(stream))
         returnValue(response)
 
+
     @inlineCallbacks
     def doSourceDelete(self):
         # Delete the source resource
@@ -425,6 +434,7 @@
         log.debug("Source removed %s" % (self.source,))
         returnValue(None)
 
+
     @inlineCallbacks
     def run(self):
         """
@@ -435,12 +445,12 @@
 
         try:
             reservation = None
-            
+
             # Handle all validation operations here.
             yield self.fullValidation()
 
             # Reservation and UID conflict checking is next.
-            if self.destinationadbk:    
+            if self.destinationadbk:
                 # Reserve UID
                 self.destination_index = self.destinationparent.index()
                 reservation = StoreAddressObjectResource.UIDReservation(
@@ -448,7 +458,7 @@
                 )
                 if self.indexdestination:
                     yield reservation.reserve()
-            
+
                 # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
                 # try to write the same vcard data to two different resource URIs.
                 result, message, rname = yield self.noUIDConflict(self.uid)
@@ -466,18 +476,109 @@
                         ),
                         "UID already used in another resource",
                     ))
-            
+
             # Do the actual put or copy
             response = (yield self.doStore())
-            
+
             if reservation:
                 yield reservation.unreserve()
-    
+
             returnValue(response)
-    
+
         except Exception, err:
 
             if reservation:
                 yield reservation.unreserve()
 
             raise err
+
+
+    @inlineCallbacks
+    def moveValidation(self):
+        """
+        Do full validation of source and destination calendar data.
+        """
+
+        # Valid resource name check
+        result, message = self.validResourceName()
+        if not result:
+            log.err(message)
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
+
+        # Valid collection size check on the destination parent resource
+        result, message = (yield self.validCollectionSize())
+        if not result:
+            log.err(message)
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                customxml.MaxResources(),
+                message,
+            ))
+
+        returnValue(None)
+
+
+    @inlineCallbacks
+    def doStoreMove(self):
+
+        # Do move
+        response = (yield self.source.storeMove(self.request, self.destinationparent, self.destination._name))
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def move(self):
+        """
+        Function that does common MOVE behavior.
+
+        @return: a Deferred with a status response result.
+        """
+
+        try:
+            reservation = None
+
+            # Handle all validation operations here.
+            yield self.moveValidation()
+
+            # Reservation and UID conflict checking is next.
+
+            # Reserve UID
+            self.destination_index = self.destinationparent.index()
+            reservation = StoreAddressObjectResource.UIDReservation(
+                self.destination_index, self.source.uid(), self.destination_uri
+            )
+            if self.indexdestination:
+                yield reservation.reserve()
+
+            # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
+            # try to write the same vcard data to two different resource URIs.
+            result, message, rname = yield self.noUIDConflict(self.source.uid())
+            if not result:
+                log.err(message)
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    NoUIDConflict(
+                        davxml.HRef.fromString(
+                            joinURL(
+                                parentForURL(self.destination_uri),
+                                rname.encode("utf-8")
+                            )
+                        )
+                    ),
+                    "UID already used in another resource",
+                ))
+
+            # Do the actual put or copy
+            response = (yield self.doStoreMove())
+
+            if reservation:
+                yield reservation.unreserve()
+
+            returnValue(response)
+
+        except Exception, err:
+
+            if reservation:
+                yield reservation.unreserve()
+
+            raise err

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/method/put_common.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -220,7 +220,7 @@
                 result, message = self.validResourceName()
                 if not result:
                     log.err(message)
-                    raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+                    raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
 
                 # Valid collection size check on the destination parent resource
                 result, message = (yield self.validCollectionSize())
@@ -229,7 +229,7 @@
                     raise HTTPError(ErrorResponse(
                         responsecode.FORBIDDEN,
                         customxml.MaxResources(),
-                        "Too many resources in collection",
+                        message,
                     ))
 
                 # Valid data sizes - do before parsing the data
@@ -241,7 +241,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "max-resource-size"),
-                            "Calendar data too large",
+                            message,
                         ))
                 else:
                     # Valid calendar data size check
@@ -251,7 +251,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "max-resource-size"),
-                            "Calendar data too large",
+                            message,
                         ))
 
             if not self.sourcecal:
@@ -263,7 +263,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "supported-calendar-data"),
-                            "Invalid content-type for data",
+                            message,
                         ))
 
                     # At this point we need the calendar data to do more tests
@@ -274,7 +274,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "valid-calendar-data"),
-                            description="Can't parse calendar data"
+                            "Can't parse calendar data"
                         ))
                 else:
                     try:
@@ -285,7 +285,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "valid-calendar-data"),
-                            description="Can't parse calendar data"
+                            "Can't parse calendar data"
                         ))
 
                 # Possible timezone stripping
@@ -302,7 +302,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "valid-calendar-data"),
-                            description=message
+                            message
                         ))
 
                     # Valid calendar data for CalDAV check
@@ -312,7 +312,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "valid-calendar-object-resource"),
-                            "Invalid calendar data",
+                            message,
                         ))
 
                     # Valid calendar component for check
@@ -322,7 +322,7 @@
                         raise HTTPError(ErrorResponse(
                             responsecode.FORBIDDEN,
                             (caldav_namespace, "supported-component"),
-                            "Invalid calendar data",
+                            message,
                         ))
 
                     # Valid attendee list size check
@@ -333,7 +333,7 @@
                             ErrorResponse(
                                 responsecode.FORBIDDEN,
                                 MaxAttendeesPerInstance.fromString(str(config.MaxAttendeesPerInstance)),
-                                "Too many attendees in calendar data",
+                                message,
                             )
                         )
 
@@ -1269,3 +1269,121 @@
                 ))
             else:
                 raise err
+
+
+    @inlineCallbacks
+    def moveValidation(self):
+        """
+        Do full validation of source and destination calendar data.
+        """
+
+        # Basic validation
+        self.validIfScheduleMatch()
+
+        # Valid resource name check
+        result, message = self.validResourceName()
+        if not result:
+            log.err(message)
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, message))
+
+        # Valid collection size check on the destination parent resource
+        result, message = (yield self.validCollectionSize())
+        if not result:
+            log.err(message)
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                customxml.MaxResources(),
+                message,
+            ))
+
+        # Check that moves to shared calendars are OK
+        yield self.validCopyMoveOperation()
+
+        returnValue(None)
+
+
+    @inlineCallbacks
+    def doStoreMove(self):
+
+        # Do move
+        response = (yield self.source.storeMove(self.request, self.destinationparent, self.destination._name))
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def move(self):
+        """
+        Function that does common MOVE behavior.
+
+        @return: a Deferred with a status response result.
+        """
+
+        try:
+            reservation = None
+
+            # Handle all validation operations here.
+            self.calendar = (yield self.source.iCalendarForUser(self.request))
+            yield self.moveValidation()
+
+            # Reservation and UID conflict checking is next.
+
+            # Reserve UID
+            self.destination_index = self.destinationparent.index()
+            reservation = StoreCalendarObjectResource.UIDReservation(
+                self.destination_index, self.source.uid(), self.destination_uri,
+                self.internal_request or self.isiTIP,
+                self.destination._associatedTransaction,
+            )
+            yield reservation.reserve()
+            # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
+            # try to write the same calendar data to two different resource URIs.
+            if not self.isiTIP:
+                result, message, rname = yield self.noUIDConflict(self.source.uid())
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(
+                        responsecode.FORBIDDEN,
+                        NoUIDConflict(
+                            davxml.HRef.fromString(
+                                joinURL(
+                                    parentForURL(self.destination_uri),
+                                    rname.encode("utf-8")
+                                )
+                            )
+                        ),
+                        "UID already exists",
+                    ))
+
+            # Do the actual put or copy
+            response = (yield self.doStoreMove())
+
+            if reservation:
+                yield reservation.unreserve()
+
+            returnValue(response)
+
+        except Exception, err:
+
+            if reservation:
+                yield reservation.unreserve()
+
+            if isinstance(err, InvalidOverriddenInstanceError):
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (caldav_namespace, "valid-calendar-data"),
+                    description="Invalid overridden instance"
+                ))
+            elif isinstance(err, TooManyInstancesError):
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    MaxInstances.fromString(str(err.max_allowed)),
+                    "Too many recurrence instances",
+                ))
+            elif isinstance(err, AttachmentStoreValidManagedID):
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (caldav_namespace, "valid-managed-id"),
+                    "Invalid Managed-ID parameter in calendar data",
+                ))
+            else:
+                raise err

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/twistedcaldav/storebridge.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -41,12 +41,13 @@
 from twext.web2.http_headers import ETag, MimeType, MimeDisposition
 from twext.web2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
 from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError
+from twext.web2.dav.resource import TwistedACLInheritable, AccessDeniedError, \
+    davPrivilegeSet
 from twext.web2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
 from twext.web2.responsecode import (
     FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
     BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE
-)
+, INTERNAL_SERVER_ERROR)
 
 from twistedcaldav import customxml, carddavxml, caldavxml
 from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin, \
@@ -69,6 +70,7 @@
 from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
 from pycalendar.datetime import PyCalendarDateTime
 import uuid
+from twext.web2.filter.location import addLocation
 
 """
 Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
@@ -148,7 +150,7 @@
     fromParent = kw.get('fromParent')
     # FIXME: direct unit tests
     def wrap(thunk):
-        def authAndContinue(self, request):
+        def authAndContinue(self, request, *args, **kwargs):
             if permissions:
                 d = self.authorize(request, permissions)
             else:
@@ -161,7 +163,7 @@
                     lambda parent:
                         parent.authorize(request, fromParent)
                 )
-            d.addCallback(lambda whatever: thunk(self, request))
+            d.addCallback(lambda whatever: thunk(self, request, *args, **kwargs))
             return d
         return authAndContinue
     return wrap
@@ -1632,8 +1634,9 @@
     def __init__(self, parent, *a, **kw):
         kw.update(principalCollections=parent.principalCollections())
         super(AttachmentsCollection, self).__init__(*a, **kw)
-        self._newStoreHome = parent._newStoreHome
-        parent.propagateTransaction(self)
+        self.parent = parent
+        self._newStoreHome = self.parent._newStoreHome
+        self.parent.propagateTransaction(self)
 
 
     def isCollection(self):
@@ -1665,7 +1668,65 @@
         return self._newStoreHome.getAllAttachmentNames()
 
 
+    def supportedPrivileges(self, request):
+        # Just DAV standard privileges - no CalDAV ones
+        return succeed(davPrivilegeSet)
 
+
+    def defaultAccessControlList(self):
+        """
+        Only read privileges allowed for managed attachments.
+        """
+        myPrincipal = self.parent.principalForRecord()
+
+        read_privs = (
+            davxml.Privilege(davxml.Read()),
+            davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+        )
+
+        aces = (
+            # Inheritable access for the resource's associated principal.
+            davxml.ACE(
+                davxml.Principal(davxml.HRef(myPrincipal.principalURL())),
+                davxml.Grant(*read_privs),
+                davxml.Protected(),
+                TwistedACLInheritable(),
+            ),
+        )
+
+        # Give read access to config.ReadPrincipals
+        aces += config.ReadACEs
+
+        # Give all access to config.AdminPrincipals
+        aces += config.AdminACEs
+
+        if config.EnableProxyPrincipals:
+            aces += (
+                # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-read/"))),
+                    davxml.Grant(*read_privs),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+                # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-write users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write/"))),
+                    davxml.Grant(*read_privs),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+            )
+
+        return davxml.ACL(*aces)
+
+
+    def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
+        # Permissions here are fixed, and are not subject to inheritance rules, etc.
+        return succeed(self.defaultAccessControlList())
+
+
+
 class CalendarAttachment(_NewStoreFileMetaDataHelper, _GetChildHelper):
 
     def __init__(self, calendarObject, attachment, attachmentName, managed, **kw):
@@ -1779,7 +1840,106 @@
         return False
 
 
+    def supportedPrivileges(self, request):
+        # Just DAV standard privileges - no CalDAV ones
+        return succeed(davPrivilegeSet)
 
+
+    @inlineCallbacks
+    def accessControlList(self, request, *a, **kw):
+        """
+        Special case managed attachments, but not dropbox (which is handled by parent collection).
+        All principals identified as ATTENDEEs on the event for this attachment
+        may read it. Also include proxies of ATTENDEEs. Ignore unknown attendees.
+        """
+
+        originalACL = yield super(CalendarAttachment, self).accessControlList(request, *a, **kw)
+        if not self._managed or not self.exists():
+            returnValue(originalACL)
+        originalACEs = list(originalACL.children)
+
+        # Look at attendees
+        if self._newStoreCalendarObject is None:
+            self._newStoreCalendarObject = (yield self._newStoreAttachment.objectResource())
+
+        cuas = (yield self._newStoreCalendarObject.component()).getAttendees()
+        newACEs = []
+        for calendarUserAddress in cuas:
+            principal = self.principalForCalendarUserAddress(
+                calendarUserAddress
+            )
+            if principal is None:
+                continue
+
+            principalURL = principal.principalURL()
+            privileges = (
+                davxml.Privilege(davxml.Read()),
+                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+            )
+            newACEs.append(davxml.ACE(
+                davxml.Principal(davxml.HRef(principalURL)),
+                davxml.Grant(*privileges),
+                davxml.Protected(),
+            ))
+            newACEs.append(davxml.ACE(
+                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-write/"))),
+                davxml.Grant(*privileges),
+                davxml.Protected(),
+            ))
+            newACEs.append(davxml.ACE(
+                davxml.Principal(davxml.HRef(joinURL(principalURL, "calendar-proxy-read/"))),
+                davxml.Grant(*privileges),
+                davxml.Protected(),
+            ))
+
+        # Now also need sharees
+        newACEs.extend((yield self.sharedManagedACEs()))
+
+        returnValue(davxml.ACL(*tuple(originalACEs + newACEs)))
+
+
+    @inlineCallbacks
+    def sharedManagedACEs(self):
+
+        aces = ()
+        calendars = yield self._newStoreCalendarObject._parentCollection.asShared()
+        for calendar in calendars:
+
+            read_privs = (
+                davxml.Privilege(davxml.Read()),
+                davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+            )
+
+            principal = self.principalForUID(calendar._home.uid())
+            aces += (
+                # Specific access for the resource's associated principal.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(principal.principalURL())),
+                    davxml.Grant(*read_privs),
+                    davxml.Protected(),
+                ),
+            )
+
+            if config.EnableProxyPrincipals:
+                aces += (
+                    # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+                    davxml.ACE(
+                        davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-read/"))),
+                        davxml.Grant(*read_privs),
+                        davxml.Protected(),
+                    ),
+                    # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
+                    davxml.ACE(
+                        davxml.Principal(davxml.HRef(joinURL(principal.principalURL(), "calendar-proxy-write/"))),
+                        davxml.Grant(*read_privs),
+                        davxml.Protected(),
+                    ),
+                )
+
+        returnValue(aces)
+
+
+
 class NoParent(CalDAVResource):
 
     def http_MKCALENDAR(self, request):
@@ -1828,6 +1988,10 @@
         return succeed(self._newStoreObject.size())
 
 
+    def uid(self):
+        return self._newStoreObject.uid()
+
+
     def component(self):
         return self._newStoreObject.component()
 
@@ -1857,6 +2021,72 @@
         return self.storeRemove(request, True, request.uri)
 
 
+    @inlineCallbacks
+    def http_MOVE(self, request):
+        """
+        MOVE for object resources.
+        """
+
+        # Do some pre-flight checks - must exist, must be move to another
+        # CommonHomeChild in the same Home, destination resource must not exist
+        if not self.exists():
+            log.debug("Resource not found: %s" % (self,))
+            raise HTTPError(NOT_FOUND)
+
+        parent = (yield request.locateResource(parentForURL(request.uri)))
+
+        #
+        # Find the destination resource
+        #
+        destination_uri = request.headers.getHeader("destination")
+        overwrite = request.headers.getHeader("overwrite", True)
+
+        if not destination_uri:
+            msg = "No destination header in MOVE request."
+            log.err(msg)
+            raise HTTPError(StatusResponse(BAD_REQUEST, msg))
+
+        destination = (yield request.locateResource(destination_uri))
+        if destination is None:
+            msg = "Destination of MOVE does not exist: %s" % (destination_uri,)
+            log.debug(msg)
+            raise HTTPError(StatusResponse(BAD_REQUEST, msg))
+        if destination.exists():
+            if overwrite:
+                msg = "Cannot overwrite existing resource with a MOVE"
+                log.debug(msg)
+                raise HTTPError(StatusResponse(FORBIDDEN, msg))
+            else:
+                msg = "Cannot MOVE to existing resource without overwrite flag enabled"
+                log.debug(msg)
+                raise HTTPError(StatusResponse(PRECONDITION_FAILED, msg))
+
+        # Check for parent calendar collection
+        destination_uri = urlsplit(destination_uri)[2]
+        destinationparent = (yield request.locateResource(parentForURL(destination_uri)))
+        if not isinstance(destinationparent, _CommonHomeChildCollectionMixin):
+            msg = "Destination of MOVE is not valid: %s" % (destination_uri,)
+            log.debug(msg)
+            raise HTTPError(StatusResponse(FORBIDDEN, msg))
+        if parentForURL(parentForURL(destination_uri)) != parentForURL(parentForURL(request.uri)):
+            msg = "Can only MOVE within the same home collection: %s" % (destination_uri,)
+            log.debug(msg)
+            raise HTTPError(StatusResponse(FORBIDDEN, msg))
+
+        #
+        # Check authentication and access controls
+        #
+        yield parent.authorize(request, (davxml.Unbind(),))
+        yield destinationparent.authorize(request, (davxml.Bind(),))
+
+        # May need to add a location header
+        addLocation(request, destination_uri)
+
+        storer = self.storeResource(request, parent, destination, destination_uri, destinationparent)
+        result = (yield storer.move())
+        returnValue(result)
+
+
     def http_PROPPATCH(self, request):
         """
         No dead properties allowed on object resources.
@@ -1867,6 +2097,13 @@
             return FORBIDDEN
 
 
+    def storeResource(self, request, parent, destination, destination_uri, destaination_parent):
+        """
+        Create the appropriate StoreXXX class for storing of data.
+        """
+        raise NotImplementedError
+
+
     @inlineCallbacks
     def storeStream(self, stream):
 
@@ -1906,6 +2143,28 @@
 
 
     @inlineCallbacks
+    def storeMove(self, request, destinationparent, destination_name):
+        """
+        Move this object to a different parent.
+
+        @param request:
+        @type request: L{twext.web2.iweb.IRequest}
+        @param destinationparent: Parent to move to
+        @type destinationparent: L{CommonHomeChild}
+        @param destination_name: name of new resource
+        @type destination_name: C{str}
+        """
+
+        try:
+            yield self._newStoreObject.moveTo(destinationparent._newStoreObject, destination_name)
+        except Exception, e:
+            log.err(e)
+            raise HTTPError(INTERNAL_SERVER_ERROR)
+
+        returnValue(CREATED)
+
+
+    @inlineCallbacks
     def storeRemove(self, request, implicitly, where):
         """
         Delete this object.
@@ -2042,6 +2301,21 @@
                 raise HTTPError(PRECONDITION_FAILED)
 
 
+    def storeResource(self, request, parent, destination, destination_uri, destaination_parent):
+        return StoreCalendarObjectResource(
+            request=request,
+            source=self,
+            source_uri=request.uri,
+            sourceparent=parent,
+            sourcecal=True,
+            deletesource=True,
+            destination=destination,
+            destination_uri=destination_uri,
+            destinationparent=destaination_parent,
+            destinationcal=True,
+        )
+
+
     @inlineCallbacks
     def storeRemove(self, request, implicitly, where):
         """
@@ -2125,6 +2399,7 @@
         returnValue(NO_CONTENT)
 
 
+    @requiresPermissions(davxml.WriteContent())
     @inlineCallbacks
     def POST_handler_attachment(self, request, action):
         """
@@ -2450,7 +2725,22 @@
     vCard = _CommonObjectResource.component
 
 
+    def storeResource(self, request, parent, destination, destination_uri, destination_parent):
+        return StoreAddressObjectResource(
+            request=request,
+            source=self,
+            source_uri=request.uri,
+            sourceparent=parent,
+            sourceadbk=True,
+            deletesource=True,
+            destination=destination,
+            destination_uri=destination_uri,
+            destinationparent=destination_parent,
+            destinationadbk=True,
+        )
 
+
+
 class _NotificationChildHelper(object):
     """
     Methods for things which are like notification objects.

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/sql.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -2223,19 +2223,19 @@
     def load(cls, txn, managedID):
         attco = schema.ATTACHMENT_CALENDAR_OBJECT
         rows = (yield Select(
-            [attco.ATTACHMENT_ID, ],
+            [attco.ATTACHMENT_ID, attco.CALENDAR_OBJECT_RESOURCE_ID, ],
             From=attco,
             Where=(attco.MANAGED_ID == managedID),
         ).on(txn))
-        aids = [row[0] for row in rows] if rows is not None else ()
-        if len(aids) == 0:
+        if len(rows) == 0:
             returnValue(None)
-        elif len(aids) != 1:
+        elif len(rows) != 1:
             raise AttachmentStoreValidManagedID
 
-        attachment = cls(txn, aids[0], None, None)
+        attachment = cls(txn, rows[0][0], None, None)
         attachment = (yield attachment.initFromStore())
         attachment._managedID = managedID
+        attachment._objectResourceID = rows[0][1]
         returnValue(attachment)
 
 
@@ -2319,6 +2319,17 @@
         return self._managedID
 
 
+    @inlineCallbacks
+    def objectResource(self):
+        """
+        Return the calendar object resource associated with this attachment.
+        """
+
+        home = (yield self._txn.calendarHomeWithResourceID(self._ownerHomeID))
+        obj = (yield home.objectResourceWithID(self._objectResourceID))
+        returnValue(obj)
+
+
     @property
     def _path(self):
         # Use directory hashing scheme based on MD5 of attachmentID

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/datastore/test/test_sql.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -30,13 +30,13 @@
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
 
 from txdav.base.propertystore.base import PropertyName
-from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests,\
+from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
     test_event_text
 from txdav.caldav.datastore.test.test_file import setUpCalendarStore
 from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
 from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource
 from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
-from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT,\
+from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
     _BIND_STATUS_ACCEPTED
 from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom
 from txdav.common.icommondatastore import NoSuchObjectResourceError
@@ -63,8 +63,9 @@
         self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
         yield self.populate()
 
-        self.nowYear = {"now":PyCalendarDateTime.getToday().getYear()}
+        self.nowYear = {"now": PyCalendarDateTime.getToday().getYear()}
 
+
     @inlineCallbacks
     def populate(self):
         yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
@@ -185,7 +186,7 @@
         backed calendar. We need to test what happens when there is "bad" calendar data
         present in the file-backed calendar with a broken recurrence-id that we can fix.
         """
-        
+
         self.storeUnderTest().setMigrating(True)
         fromCalendar = yield (yield self.fileTransaction().calendarHomeWithUID(
             "home_bad")).calendarWithName("calendar_fix_recurrence")
@@ -306,7 +307,7 @@
 END:VEVENT
 END:VCALENDAR
 """.replace("\n", "\r\n") % self.nowYear)
-        
+
         toResource = yield toCalendar.calendarObjectWithName("3.ics")
         caldata = yield toResource.component()
         self.assertEqual(str(caldata), """BEGIN:VCALENDAR
@@ -353,7 +354,8 @@
 END:VEVENT
 END:VCALENDAR
 """.replace("\n", "\r\n") % self.nowYear)
-        
+
+
     @inlineCallbacks
     def test_migrateDuplicateAttachmentsCalendarFromFile(self):
         """
@@ -371,6 +373,7 @@
         self.assertEqual(ok, 3)
         self.assertEqual(bad, 0)
 
+
     @inlineCallbacks
     def test_migrateCalendarFromFile_Transparency(self):
         """
@@ -385,10 +388,10 @@
         yield _migrateCalendar(fromCalendar, toCalendar,
                                lambda x: x.component())
 
-        filter =  caldavxml.Filter(
+        filter = caldavxml.Filter(
                       caldavxml.ComponentFilter(
                           caldavxml.ComponentFilter(
-                              caldavxml.TimeRange(start="%(now)s0201T000000Z"  % self.nowYear, end="%(now)s0202T000000Z" % self.nowYear),
+                              caldavxml.TimeRange(start="%(now)s0201T000000Z" % self.nowYear, end="%(now)s0202T000000Z" % self.nowYear),
                               name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
                           ),
                           name="VCALENDAR",
@@ -403,6 +406,7 @@
         self.assertEquals(uid, "uid4")
         self.assertEquals(transp, 'T')
 
+
     @inlineCallbacks
     def test_migrateHomeFromFile(self):
         """
@@ -410,7 +414,7 @@
         backend to another; in this specific case, from the file-based backend
         to the SQL-based backend.
         """
-        
+
         # Need to turn of split calendar behavior just for this test
         self.patch(config, "RestrictCalendarsToOneComponentType", False)
 
@@ -467,9 +471,10 @@
                 continue
             result = yield calendar.getSupportedComponents()
             supported_components.add(result)
-            
+
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     @inlineCallbacks
     def test_migrateHomeNoSplits(self):
         """
@@ -494,24 +499,25 @@
                 continue
             result = yield calendar.getSupportedComponents()
             supported_components.add(result)
-            
+
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     def test_calendarHomeVersion(self):
         """
         The DATAVERSION column for new calendar homes must match the
         CALENDAR-DATAVERSION value.
         """
-        
+
         home = yield self.transactionUnderTest().calendarHomeWithUID("home_version")
         self.assertTrue(home is not None)
         yield self.transactionUnderTest().commit
-        
+
         txn = yield self.transactionUnderTest()
         version = yield txn.calendarserverValue("CALENDAR-DATAVERSION")[0][0]
         ch = schema.CALENDAR_HOME
         homeVersion = yield Select(
-            [ch.DATAVERSION,],
+            [ch.DATAVERSION, ],
             From=ch,
             Where=ch.OWNER_UID == "home_version",
         ).on(txn)[0][0]
@@ -695,6 +701,7 @@
         yield d1
         yield d2
 
+
     @inlineCallbacks
     def test_datetimes(self):
         calendarStore = self._sqlCalendarStore
@@ -714,6 +721,7 @@
         self.assertEqual(obj.created(), datetimeMktime(datetime.datetime(2011, 2, 7, 11, 22, 47)))
         self.assertEqual(obj.modified(), datetimeMktime(datetime.datetime(2011, 2, 8, 11, 22, 47)))
 
+
     @inlineCallbacks
     def test_notificationsProvisioningConcurrency(self):
         """
@@ -753,6 +761,7 @@
         self.assertNotEqual(notification_uid1_1, None)
         self.assertNotEqual(notification_uid1_2, None)
 
+
     @inlineCallbacks
     def test_removeCalendarPropertiesOnDelete(self):
         """
@@ -766,7 +775,7 @@
         calendar = yield home.createCalendarWithName(name)
         resourceID = calendar._resourceID
         calendarProperties = calendar.properties()
-        
+
         prop = caldavxml.CalendarDescription.fromString("Calendar to be removed")
         calendarProperties[PropertyName.fromElement(prop)] = prop
         yield self.commit()
@@ -794,6 +803,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_removeCalendarObjectPropertiesOnDelete(self):
         """
@@ -839,6 +849,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_removeInboxObjectPropertiesOnDelete(self):
         """
@@ -849,7 +860,7 @@
         # Create calendar object and add a property
         home = yield self.homeUnderTest()
         inbox = yield home.createCalendarWithName("inbox")
-        
+
         name = "test.ics"
         component = VComponent.fromString(test_event_text)
         metadata = {
@@ -891,6 +902,7 @@
         self.assertEqual(len(tuple(rows)), 0)
         yield self.commit()
 
+
     @inlineCallbacks
     def test_directShareCreateConcurrency(self):
         """
@@ -942,22 +954,23 @@
         yield d1
         yield d2
 
+
     @inlineCallbacks
     def test_transferSharingDetails(self):
         """
         Test Calendar._transferSharingDetails to make sure sharing details are transferred.
         """
-        
+
         shareeHome = yield self.transactionUnderTest().calendarHomeWithUID("home_splits_shared")
 
         calendar = yield (yield self.transactionUnderTest().calendarHomeWithUID(
             "home_splits")).calendarWithName("calendar_1")
-        
+
         # Fake a shared binding on the original calendar
         bind = calendar._bindSchema
         _bindCreate = Insert({
             bind.HOME_RESOURCE_ID: shareeHome._resourceID,
-            bind.RESOURCE_ID: calendar._resourceID, 
+            bind.RESOURCE_ID: calendar._resourceID,
             bind.RESOURCE_NAME: "shared_1",
             bind.MESSAGE: "Shared to you",
             bind.BIND_MODE: _BIND_MODE_DIRECT,
@@ -984,34 +997,36 @@
         self.assertTrue(sharedCalendar is not None)
         self.assertEqual(sharedCalendar._resourceID, newcalendar._resourceID)
 
+
     @inlineCallbacks
     def test_moveCalendarObjectResource(self):
         """
         Test Calendar._transferSharingDetails to make sure sharing details are transferred.
         """
-        
+
         calendar1 = yield (yield self.transactionUnderTest().calendarHomeWithUID(
             "home_splits")).calendarWithName("calendar_1")
         calendar2 = yield (yield self.transactionUnderTest().calendarHomeWithUID(
             "home_splits")).calendarWithName("calendar_2")
-        
+
         child = yield calendar2.calendarObjectWithName("5.ics")
-        
+
         yield calendar2.moveObjectResource(child, calendar1)
-        
+
         child = yield calendar2.calendarObjectWithName("5.ics")
         self.assertTrue(child is None)
-        
+
         child = yield calendar1.calendarObjectWithName("5.ics")
         self.assertTrue(child is not None)
 
+
     @inlineCallbacks
     def test_splitCalendars(self):
         """
         Test Calendar.splitCollectionByComponentTypes to make sure components are split out,
         sync information is updated.
         """
-        
+
         # calendar_2 add a dead property to make sure it gets copied over
         home = yield self.transactionUnderTest().calendarHomeWithUID("home_splits")
         calendar2 = yield home.calendarWithName("calendar_2")
@@ -1031,7 +1046,7 @@
         child = yield home.calendarWithName("calendar_1-vtodo")
         self.assertTrue(child is None)
 
-        calendar1 = yield home.calendarWithName("calendar_1")        
+        calendar1 = yield home.calendarWithName("calendar_1")
         children = yield calendar1.listCalendarObjects()
         self.assertEqual(len(children), 3)
         new_sync_token1 = yield calendar1.syncToken()
@@ -1043,7 +1058,7 @@
 
         # calendar_2 does split
         home = yield self.transactionUnderTest().calendarHomeWithUID("home_splits")
-        calendar2 = yield home.calendarWithName("calendar_2")        
+        calendar2 = yield home.calendarWithName("calendar_2")
         original_sync_token2 = yield calendar2.syncToken()
         yield calendar2.splitCollectionByComponentTypes()
         yield self.commit()
@@ -1062,7 +1077,7 @@
         self.assertTrue(pkey in calendar2_vtodo.properties())
         self.assertEqual(str(calendar2_vtodo.properties()[pkey]), "A birthday calendar")
 
-        calendar2 = yield home.calendarWithName("calendar_2")        
+        calendar2 = yield home.calendarWithName("calendar_2")
         children = yield calendar2.listCalendarObjects()
         self.assertEqual(len(children), 3)
         new_sync_token2 = yield calendar2.syncToken()
@@ -1075,13 +1090,14 @@
         self.assertTrue(pkey in calendar2.properties())
         self.assertEqual(str(calendar2.properties()[pkey]), "A birthday calendar")
 
+
     @inlineCallbacks
     def test_noSplitCalendars(self):
         """
         Test CalendarHome.splitCalendars to make sure we end up with at least two collections
         with different supported components.
         """
-        
+
         # Do split
         home = yield self.transactionUnderTest().calendarHomeWithUID("home_no_splits")
         calendars = yield home.calendars()
@@ -1098,23 +1114,24 @@
                 continue
             result = yield calendar.getSupportedComponents()
             supported_components.add(result)
-            
+
         self.assertEqual(supported_components, set(("VEVENT", "VTODO",)))
 
+
     @inlineCallbacks
     def test_resourceLock(self):
         """
         Test CommonObjectResource.lock to make sure it locks, raises on missing resource,
         and raises when locked and wait=False used.
         """
-        
+
         # Valid object
         resource = yield self.calendarObjectUnderTest()
-        
+
         # Valid lock
         yield resource.lock()
         self.assertTrue(resource._locked)
-        
+
         # Setup a new transaction to verify the lock and also verify wait behavior
         newTxn = self._sqlCalendarStore.newTransaction()
         newResource = yield self.calendarObjectUnderTest(txn=newTxn)
@@ -1129,19 +1146,19 @@
 
         # Commit existing transaction and verify we can get the lock using
         yield self.commit()
-        
+
         resource = yield self.calendarObjectUnderTest()
         yield resource.lock()
         self.assertTrue(resource._locked)
-                
+
         # Setup a new transaction to verify the lock but pass in an alternative txn directly
         newTxn = self._sqlCalendarStore.newTransaction()
-        
+
         # FIXME: not sure why, but without this statement here, this portion of the test fails in a funny way.
         # Basically the query in the try block seems to execute twice, failing each time, one of which is caught,
         # and the other not - causing the test to fail. Seems like some state on newTxn is not being initialized?
         yield self.calendarObjectUnderTest("2.ics", txn=newTxn)
-        
+
         try:
             yield resource.lock(wait=False, useTxn=newTxn)
         except:
@@ -1169,29 +1186,30 @@
         """
         Test CalendarObjectResource.recurrenceMinMax to make sure it handles a None value.
         """
-        
+
         # Valid object
         resource = yield self.calendarObjectUnderTest()
-        
+
         # Valid lock
         rMin, rMax = yield resource.recurrenceMinMax()
         self.assertEqual(rMin, None)
         self.assertEqual(rMax, None)
 
+
     @inlineCallbacks
     def test_notExpandedWithin(self):
         """
         Test PostgresLegacyIndexEmulator.notExpandedWithin to make sure it returns the correct
         result based on the ranges passed in.
         """
-        
+
         self.patch(config, "FreeBusyIndexDelayedExpand", False)
 
         # Create the index on a new calendar
         home = yield self.homeUnderTest()
         newcalendar = yield home.createCalendarWithName("index_testing")
         index = PostgresLegacyIndexEmulator(newcalendar)
-        
+
         # Create the calendar object to use for testing
         nowYear = self.nowYear["now"]
         caldata = """BEGIN:VCALENDAR
@@ -1287,15 +1305,15 @@
         instances = yield calendarObject.instances()
         self.assertNotEqual(len(instances), 0)
         yield self.commit()
-        
+
         # Re-add event with re-indexing
         calendar = yield self.calendarUnderTest()
         calendarObject = yield self.calendarObjectUnderTest("indexing.ics")
         yield calendarObject.setComponent(component)
         instances2 = yield calendarObject.instances()
         self.assertNotEqual(
-            sorted(instances, key=lambda x:x[0])[0], 
-            sorted(instances2, key=lambda x:x[0])[0], 
+            sorted(instances, key=lambda x: x[0])[0],
+            sorted(instances2, key=lambda x: x[0])[0],
         )
         yield self.commit()
 
@@ -1306,13 +1324,14 @@
         yield calendarObject.setComponent(component)
         instances3 = yield calendarObject.instances()
         self.assertEqual(
-            sorted(instances2, key=lambda x:x[0])[0], 
-            sorted(instances3, key=lambda x:x[0])[0], 
+            sorted(instances2, key=lambda x: x[0])[0],
+            sorted(instances3, key=lambda x: x[0])[0],
         )
-        
+
         yield calendar.removeCalendarObjectWithName("indexing.ics")
         yield self.commit()
 
+
     @inlineCallbacks
     def test_loadObjectResourcesWithName(self):
         """
@@ -1324,19 +1343,19 @@
         def _tests(cal):
             resources = yield cal.objectResourcesWithNames(("1.ics",))
             self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics",)))
-    
+
             resources = yield cal.objectResourcesWithNames(("1.ics", "2.ics",))
             self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics",)))
-    
+
             resources = yield cal.objectResourcesWithNames(("1.ics", "2.ics", "3.ics",))
             self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics", "3.ics",)))
-    
+
             resources = yield cal.objectResourcesWithNames(("1.ics", "2.ics", "3.ics", "4.ics",))
             self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics", "3.ics", "4.ics",)))
-    
+
             resources = yield cal.objectResourcesWithNames(("bogus1.ics",))
             self.assertEqual(set([resource.name() for resource in resources]), set())
-    
+
             resources = yield cal.objectResourcesWithNames(("bogus1.ics", "2.ics",))
             self.assertEqual(set([resource.name() for resource in resources]), set(("2.ics",)))
 
@@ -1347,7 +1366,7 @@
         # Adjust batch size and try again
         self.patch(CommonObjectResource, "BATCH_LOAD_SIZE", 2)
         yield _tests(cal)
-        
+
         yield self.commit()
 
         # Tests on inbox - resources with properties
@@ -1392,13 +1411,27 @@
         self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
 
         resources = yield inbox.objectResourcesWithNames(("1.ics", "2.ics",))
-        resources.sort(key=lambda x:x._name)
+        resources.sort(key=lambda x: x._name)
         prop = caldavxml.CalendarDescription.fromString("p1")
         self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
         prop = caldavxml.CalendarDescription.fromString("p2")
         self.assertEqual(resources[1].properties()[PropertyName.fromElement(prop)], prop)
 
         resources = yield inbox.objectResourcesWithNames(("bogus1.ics", "2.ics",))
-        resources.sort(key=lambda x:x._name)
+        resources.sort(key=lambda x: x._name)
         prop = caldavxml.CalendarDescription.fromString("p2")
         self.assertEqual(resources[0].properties()[PropertyName.fromElement(prop)], prop)
+
+
+    @inlineCallbacks
+    def test_objectResourceWithID(self):
+        """
+        L{ICalendarHome.objectResourceWithID} will return the calendar object..
+        """
+        home = yield self.homeUnderTest()
+        calendarObject = (yield home.objectResourceWithID(9999))
+        self.assertEquals(calendarObject, None)
+
+        obj = (yield self.calendarObjectUnderTest())
+        calendarObject = (yield home.objectResourceWithID(obj._resourceID))
+        self.assertNotEquals(calendarObject, None)

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/caldav/icalendarstore.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -298,7 +298,19 @@
         """
 
 
+    def objectResourceWithID(rid):
+        """
+        Return the calendar object resource with the specified ID, assumed to be a child of
+        a calendar collection within this home.
 
+        @param rid: resource id of object to find
+        @type rid: C{int}
+
+        @return: L{ICalendar} or C{None} if not found
+        """
+
+
+
 class ICalendar(INotifier, IShareableCollection, IDataStoreObject):
     """
     Calendar

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/file.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -291,7 +291,7 @@
         returnValue([kv[1] for kv in sorted(self._determineMemo(storeType, None).items(), key=lambda x: x[0])])
 
 
-    @memoizedKey("uid", _determineMemo)
+    @memoizedKey("uid", _determineMemo, deferredResult=False)
     def homeWithUID(self, storeType, uid, create=False):
         if uid.startswith("."):
             return None
@@ -302,7 +302,7 @@
         return self._homeClass[storeType].homeWithUID(self, uid, create, storeType == ECALENDARTYPE)
 
 
-    @memoizedKey("uid", "_notificationHomes")
+    @memoizedKey("uid", "_notificationHomes", deferredResult=False)
     def notificationsWithUID(self, uid, home=None):
 
         if home is None:
@@ -678,6 +678,15 @@
         return results
 
 
+    def objectResourceWithID(self, rid):
+        """
+        Return all child object resources with the specified resource-ID.
+        """
+
+        # File store does not have resource ids.
+        raise NotImplementedError
+
+
     def quotaUsedBytes(self):
 
         try:

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/sql.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -1009,13 +1009,13 @@
         """
 
         # TODO: see if there is a better way to import Attachment
-        from txdav.caldav.datastore.sql import Attachment
+        from txdav.caldav.datastore.sql import DropBoxAttachment
 
         results = (yield self.orphanedAttachments(batchSize=batchSize))
         count = 0
         for dropboxID, path in results:
-            attachment = Attachment(self, dropboxID, path)
-            (yield attachment.remove())
+            attachment = (yield DropBoxAttachment.load(self, dropboxID, path))
+            yield attachment.remove()
             count += 1
         returnValue(count)
 
@@ -1590,6 +1590,34 @@
         returnValue(results)
 
 
+    @classmethod
+    def _objectResourceIDQuery(cls):
+        obj = cls._objectSchema
+        return Select(
+            [obj.PARENT_RESOURCE_ID],
+            From=obj,
+            Where=(obj.RESOURCE_ID == Parameter("resourceID")),
+        )
+
+
+    @inlineCallbacks
+    def objectResourceWithID(self, rid):
+        """
+        Return all child object resources with the specified resource-ID.
+        """
+        rows = (yield self._objectResourceIDQuery().on(
+            self._txn, resourceID=rid
+        ))
+        if rows and len(rows) == 1:
+            child = (yield self.childWithID(rows[0][0]))
+            objectResource = (
+                yield child.objectResourceWithID(rid)
+            )
+            returnValue(objectResource)
+
+        returnValue(None)
+
+
     @classproperty
     def _quotaQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
@@ -3291,13 +3319,18 @@
 
 
     @classproperty
-    def _moveParentUpdateQuery(cls): #@NoSelf
+    def _moveParentUpdateQuery(cls, adjustName=False): #@NoSelf
         """
         DAL query to update a child to be in a new parent.
         """
         obj = cls._objectSchema
+        cols = {
+            obj.PARENT_RESOURCE_ID: Parameter("newParentID")
+        }
+        if adjustName:
+            cols[obj.RESOURCE_NAME] = Parameter("newName")
         return Update(
-            {obj.PARENT_RESOURCE_ID: Parameter("newParentID")},
+            cols,
             Where=obj.RESOURCE_ID == Parameter("resourceID")
         )
 
@@ -3311,7 +3344,7 @@
 
 
     @inlineCallbacks
-    def moveObjectResource(self, child, newparent):
+    def moveObjectResource(self, child, newparent, newname=None):
         """
         Move a child of this collection into another collection without actually removing/re-inserting the data.
         Make sure sync and cache details for both collections are updated.
@@ -3323,11 +3356,19 @@
         @type child: L{CommonObjectResource}
         @param newparent: the parent to move to
         @type newparent: L{CommonHomeChild}
+        @param newname: new name to use in new parent
+        @type newname: C{str} or C{None} for existing name
         """
 
+        if newname and newname.startswith("."):
+            raise ObjectResourceNameNotAllowedError(newname)
+
         name = child.name()
         uid = child.uid()
 
+        if newname is None:
+            newname = name
+
         # Clean this collections cache and signal sync change
         self._objects.pop(name, None)
         self._objects.pop(uid, None)
@@ -3335,17 +3376,32 @@
         yield self._deleteRevision(name)
         yield self.notifyChanged()
 
-        # Adjust the child to be a child of the new parent and update ancillary tables
-        yield self._moveParentUpdateQuery.on(
+        # Handle cases where move is within the same collection or to a different collection
+        # with/without a name change
+        obj = self._objectSchema
+        cols = {}
+        if newparent._resourceID != self._resourceID:
+            cols[obj.PARENT_RESOURCE_ID] = Parameter("newParentID")
+        if newname != name:
+            cols[obj.RESOURCE_NAME] = Parameter("newName")
+        yield Update(
+            cols,
+            Where=obj.RESOURCE_ID == Parameter("resourceID")
+        ).on(
             self._txn,
+            resourceID=child._resourceID,
             newParentID=newparent._resourceID,
-            resourceID=child._resourceID
+            newName=newname,
         )
-        yield self._movedObjectResource(child, newparent)
+
+        # Only signal a move when parent is different
+        if newparent._resourceID != self._resourceID:
+            yield self._movedObjectResource(child, newparent)
+
         child._parentCollection = newparent
 
         # Signal sync change on new collection
-        yield newparent._insertRevision(name)
+        yield newparent._insertRevision(newname)
         yield newparent.notifyChanged()
 
 
@@ -3859,6 +3915,21 @@
         return Delete(cls._objectSchema, Where=cls._objectSchema.RESOURCE_ID == Parameter("resourceID"))
 
 
+    def moveTo(self, destination, name):
+        """
+        Move object to another collection.
+
+        @param destination: parent collection to move to
+        @type destination: L{CommonHomeChild}
+        @param name: new name in destination
+        @type name: C{str} or C{None} to use existing name
+        """
+
+        if name and name.startswith("."):
+            raise ObjectResourceNameNotAllowedError(name)
+        return self._parentCollection.moveObjectResource(self, destination, name)
+
+
     @inlineCallbacks
     def remove(self):
         yield self._deleteQuery.on(self._txn, NoSuchObjectResourceError,

Modified: CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py	2012-12-04 16:47:12 UTC (rev 10121)
+++ CalendarServer/branches/users/cdaboo/managed-attachments/txdav/common/datastore/test/util.py	2012-12-04 16:48:48 UTC (rev 10122)
@@ -126,7 +126,8 @@
         reactor.addSystemEventTrigger("before", "shutdown", cp.stopService)
         cds = CommonDataStore(
             cp.connection, StubNotifierFactory(),
-            attachmentRoot, quota=staticQuota
+            attachmentRoot, "",
+            quota=staticQuota
         )
         return cds
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121204/ad98eefa/attachment-0001.html>


More information about the calendarserver-changes mailing list