[CalendarServer-changes] [11032] CalendarServer/branches/users/cdaboo/store-scheduling

source_changes at macosforge.org source_changes at macosforge.org
Fri Apr 12 11:26:01 PDT 2013


Revision: 11032
          http://trac.calendarserver.org//changeset/11032
Author:   cdaboo at apple.com
Date:     2013-04-12 11:26:01 -0700 (Fri, 12 Apr 2013)
Log Message:
-----------
Checkpoint - implemented http_PUT and http_DELETE for calendar objects using store apis.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/instance.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_resource.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/instance.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/instance.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/instance.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -33,6 +33,10 @@
         self.max_allowed = config.MaxAllowedInstances
 
 
+    def __str__(self):
+        return "Too many recurrence instances."
+
+
     def __repr__(self):
         return "<%s max:%s>" % (self.__class__.__name__, self.max_allowed)
 
@@ -45,6 +49,10 @@
         self.rid = rid
 
 
+    def __str__(self):
+        return "Invalid overridden instance :%s" % (self.rid,)
+
+
     def __repr__(self):
         return "<%s invalid:%s>" % (self.__class__.__name__, self.rid)
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -39,6 +39,8 @@
     # index file has the entry for the deleted calendar component removed.
     #
 
+    raise AssertionError("Never use this")
+
     if not self.exists():
         log.err("Resource not found: %s" % (self,))
         raise HTTPError(responsecode.NOT_FOUND)

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/delete_common.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -32,6 +32,8 @@
     def __init__(self, request, resource, resource_uri, parent, depth,
         internal_request=False, allowImplicitSchedule=True):
 
+        raise AssertionError("Never use this")
+
         self.request = request
         self.resource = resource
         self.resource_uri = resource_uri
@@ -47,7 +49,7 @@
         # with storeRemove on them also have their own http_DELETEs.
         response = (
             yield self.resource.storeRemove(
-                self.request, 
+                self.request,
                 not self.internal_request and self.allowImplicitSchedule,
                 self.resource_uri
             )

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -42,6 +42,8 @@
 @inlineCallbacks
 def http_PUT(self, request):
 
+    raise AssertionError("Never use this")
+
     parentURL = parentForURL(request.uri)
     parent = (yield request.locateResource(parentURL))
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_addressbook_common.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -113,6 +113,8 @@
         @param returnData:         True if the caller wants the actual data written to the store returned
         """
 
+        raise AssertionError("Never use this")
+
         # Check that all arguments are valid
         try:
             assert destination is not None and destinationparent is not None and destination_uri is not None

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/method/put_common.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -159,6 +159,8 @@
         @param attachmentProcessingDone    True if the caller has already processed managed attachment changes
         """
 
+        raise AssertionError("Never use this")
+
         # Check that all arguments are valid
         try:
             assert destination is not None and destinationparent is not None and destination_uri is not None

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/storebridge.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -41,13 +41,14 @@
 from twistedcaldav import customxml, carddavxml, caldavxml
 from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin, \
     DisabledCacheNotifier
-from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.caldavxml import caldav_namespace, MaxAttendeesPerInstance, \
+    NoUIDConflict, MaxInstances
 from twistedcaldav.carddavxml import carddav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.directory.wiki import WikiDirectoryService, getWikiAccess
 from twistedcaldav.ical import Component as VCalendar, Property as VProperty, \
-    InvalidICalendarDataError, iCalendarProductID, allowedComponents
-from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
+    InvalidICalendarDataError, iCalendarProductID, allowedComponents, Component
+from twistedcaldav.memcachelock import MemcacheLockTimeoutError
 from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
@@ -60,10 +61,16 @@
 from txdav.base.propertystore.base import PropertyName
 from txdav.caldav.icalendarstore import QuotaExceeded, AttachmentStoreFailed, \
     AttachmentStoreValidManagedID, AttachmentRemoveFailed, \
-    AttachmentDropboxNotAllowed
+    AttachmentDropboxNotAllowed, InvalidComponentTypeError, \
+    TooManyAttendeesError, InvalidCalendarAccessError, ValidOrganizerError, \
+    UIDExistsError, InvalidUIDError, InvalidPerUserDataMerge, \
+    AttendeeAllowedError, ResourceDeletedError, InvalidComponentForStoreError
 from txdav.common.datastore.sql_tables import _BIND_MODE_READ, _BIND_MODE_WRITE, \
     _BIND_MODE_DIRECT
-from txdav.common.icommondatastore import NoSuchObjectResourceError
+from txdav.common.icommondatastore import NoSuchObjectResourceError, \
+    TooManyObjectResourcesError, ObjectResourceTooBigError, \
+    InvalidObjectResourceError, ObjectResourceNameNotAllowedError, \
+    ObjectResourceNameAlreadyExistsError
 from txdav.idav import PropertyChangeNotAllowedError
 from txdav.xml import element as davxml
 from txdav.xml.base import dav_namespace, WebDAVUnknownElement, encodeXMLName
@@ -72,6 +79,12 @@
 import hashlib
 import time
 import uuid
+from twext.web2 import responsecode
+from twext.web2.iweb import IResponse
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.instance import InvalidOverriddenInstanceError, \
+    TooManyInstancesError
+from twisted.python.failure import Failure
 
 """
 Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
@@ -417,12 +430,12 @@
             )
             log.err(msg)
             raise HTTPError(StatusResponse(BAD_REQUEST, msg))
-        response = (yield self.storeRemove(request, True, request.uri))
+        response = (yield self.storeRemove(request))
         returnValue(response)
 
 
     @inlineCallbacks
-    def storeRemove(self, request, viaRequest, where):
+    def storeRemove(self, request):
         """
         Delete this collection resource, first deleting each contained
         object resource.
@@ -462,11 +475,11 @@
         # 'deluri' is this resource's URI; I should be able to synthesize it
         # from 'self'.
 
-        errors = ResponseQueue(where, "DELETE", NO_CONTENT)
+        errors = ResponseQueue(request.uri, "DELETE", NO_CONTENT)
 
         for childname in (yield self.listChildren()):
 
-            childurl = joinURL(where, childname)
+            childurl = joinURL(request.uri, childname)
 
             # FIXME: use a more specific API; we should know what this child
             # resource is, and not have to look it up.  (Sharing information
@@ -474,7 +487,7 @@
             child = (yield request.locateChildResource(self, childname))
 
             try:
-                yield child.storeRemove(request, viaRequest, childurl)
+                yield child.storeRemove(request)
             except:
                 logDefaultException()
                 errors.add(childurl, BAD_REQUEST)
@@ -902,11 +915,7 @@
             if ifmatch and ifmatch != etag.generate():
                 raise HTTPError(PRECONDITION_FAILED)
 
-            yield deleteResource.storeRemove(
-                request,
-                True,
-                href,
-            )
+            yield deleteResource.storeRemove(request)
 
         except HTTPError, e:
             # Extract the pre-condition
@@ -1193,7 +1202,7 @@
 
 
     @inlineCallbacks
-    def storeRemove(self, request, implicitly, where):
+    def storeRemove(self, request):
         """
         Delete this calendar collection resource, first deleting each contained
         calendar resource.
@@ -1207,14 +1216,6 @@
 
         @type request: L{twext.web2.iweb.IRequest}
 
-        @param implicitly: Should implicit scheduling operations be triggered
-            as a resut of this C{DELETE}?
-
-        @type implicitly: C{bool}
-
-        @param where: the URI at which the resource is being deleted.
-        @type where: C{str}
-
         @return: an HTTP response suitable for sending to a client (or
             including in a multi-status).
 
@@ -1232,9 +1233,7 @@
             ))
 
         response = (
-            yield super(CalendarCollectionResource, self).storeRemove(
-                request, implicitly, where
-            )
+            yield super(CalendarCollectionResource, self).storeRemove(request)
         )
 
         if response == NO_CONTENT:
@@ -2176,7 +2175,7 @@
             log.debug("Resource not found: %s" % (self,))
             raise HTTPError(NOT_FOUND)
 
-        return self.storeRemove(request, True, request.uri)
+        return self.storeRemove(request)
 
 
     @inlineCallbacks
@@ -2311,7 +2310,7 @@
 
 
     @inlineCallbacks
-    def storeRemove(self, request, implicitly, where):
+    def storeRemove(self, request):
         """
         Delete this object.
 
@@ -2464,108 +2463,152 @@
                     "If-Schedule-Tag-Match: header value '%s' does not match resource value '%s'" %
                     (header, self.scheduleTag,))
                 raise HTTPError(PRECONDITION_FAILED)
+            return True
 
+        elif config.Scheduling.CalDAV.ScheduleTagCompatibility:
+            # Compatibility with old clients. Policy:
+            #
+            # 1. If If-Match header is not present, never do smart merge.
+            # 2. If If-Match is present and the specified ETag is
+            #    considered a "weak" match to the current Schedule-Tag,
+            #    then do smart merge, else reject with a 412.
+            #
+            # Actually by the time we get here the precondition will
+            # already have been tested and found to be OK, so we can just
+            # always do smart merge now if If-Match is present.
+            return request.headers.getHeader("If-Match") is not None
 
-    def storeResource(self, request, parent, destination, destination_uri, destination_parent, hasSource, component, attachmentProcessingDone=False):
-        return StoreCalendarObjectResource(
-            request=request,
-            source=self if hasSource else None,
-            source_uri=request.uri if hasSource else None,
-            sourceparent=parent if hasSource else None,
-            sourcecal=hasSource,
-            deletesource=hasSource,
-            destination=destination,
-            destination_uri=destination_uri,
-            destinationparent=destination_parent,
-            destinationcal=True,
-            calendar=component,
-            attachmentProcessingDone=attachmentProcessingDone,
-        )
+        else:
+            return False
 
+    StoreExceptionsStatusErrors = set((
+        ObjectResourceNameNotAllowedError,
+        ObjectResourceNameAlreadyExistsError,
+    ))
 
+    StoreExceptionsErrors = {
+        TooManyObjectResourcesError: customxml.MaxResources(),
+        ObjectResourceTooBigError: (caldav_namespace, "max-resource-size"),
+        InvalidObjectResourceError: (caldav_namespace, "valid-calendar-data"),
+        InvalidComponentForStoreError: (caldav_namespace, "valid-calendar-object-resource"),
+        InvalidComponentTypeError: (caldav_namespace, "supported-component"),
+        TooManyAttendeesError: MaxAttendeesPerInstance.fromString(str(config.MaxAttendeesPerInstance)),
+        InvalidCalendarAccessError: (calendarserver_namespace, "valid-access-restriction"),
+        ValidOrganizerError: (calendarserver_namespace, "valid-organizer"),
+        UIDExistsError: NoUIDConflict(),
+        InvalidUIDError: NoUIDConflict(),
+        InvalidPerUserDataMerge: (caldav_namespace, "valid-calendar-data"),
+        AttendeeAllowedError: (caldav_namespace, "attendee-allowed"),
+        InvalidOverriddenInstanceError: (caldav_namespace, "valid-calendar-data"),
+        TooManyInstancesError: MaxInstances.fromString(str(config.MaxAllowedInstances)),
+        AttachmentStoreValidManagedID: (caldav_namespace, "valid-managed-id"),
+    }
+
     @inlineCallbacks
-    def storeRemove(self, request, implicitly, where):
-        """
-        Delete this calendar object and do implicit scheduling actions if
-        required.
+    def http_PUT(self, request):
 
-        @param request: Unused by this implementation; present for signature
-            compatibility with L{CalendarCollectionResource.storeRemove}.
+        # Content-type check
+        content_type = request.headers.getHeader("content-type")
+        if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
+            log.err("MIME type %s not allowed in calendar collection" % (content_type,))
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (caldav_namespace, "supported-calendar-data"),
+                "Invalid MIME type for calendar collection",
+            ))
 
-        @type request: L{twext.web2.iweb.IRequest}
+        # Do schedule tag check
+        schedule_tag_match = self.validIfScheduleMatch(request)
 
-        @param implicitly: Should implicit scheduling operations be triggered
-            as a result of this C{DELETE}?
+        # Read the calendar component from the stream
+        try:
+            calendardata = (yield allDataFromStream(request.stream))
+            if not hasattr(request, "extendedLogItems"):
+                request.extendedLogItems = {}
+            request.extendedLogItems["cl"] = str(len(calendardata)) if calendardata else "0"
 
-        @type implicitly: C{bool}
+            # We must have some data at this point
+            if calendardata is None:
+                # Use correct DAV:error response
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (caldav_namespace, "valid-calendar-data"),
+                    description="No calendar data"
+                ))
 
-        @param where: the URI at which the resource is being deleted.
-        @type where: C{str}
+            try:
+                component = Component.fromString(calendardata)
+            except ValueError, e:
+                log.err(str(e))
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (caldav_namespace, "valid-calendar-data"),
+                    "Can't parse calendar data"
+                ))
 
-        @return: an HTTP response suitable for sending to a client (or
-            including in a multi-status).
+            try:
+                response = (yield self.storeComponent(component))
+            except ResourceDeletedError:
+                # This is OK - it just means the server deleted the resource during the PUT. We make it look
+                # like the PUT succeeded.
+                response = responsecode.CREATED if self.exists() else responsecode.NO_CONTENT
+            response = IResponse(response)
 
-         @rtype: something adaptable to L{twext.web2.iweb.IResponse}
-        """
+            # Look for Prefer header
+            prefer = request.headers.getHeader("prefer", {})
+            returnRepresentation = any([key == "return" and value == "representation" for key, value, _ignore_args in prefer])
 
-        # 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 returnRepresentation and response.code / 100 == 2:
+                oldcode = response.code
+                response = (yield self.http_GET(request))
+                if oldcode == responsecode.CREATED:
+                    response.code = responsecode.CREATED
+                response.headers.setHeader("content-location", request.path)
 
-        isinbox = self._newStoreObject._calendar.name() == "inbox"
-        transaction = self._newStoreObject.transaction()
+            returnValue(response)
 
-        # Do If-Schedule-Tag-Match behavior first
-        # Important: this should only ever be done when storeRemove is called
-        # directly as a result of an HTTP DELETE to ensure the proper If-
-        # header is used in this test.
-        if not isinbox and implicitly:
-            self.validIfScheduleMatch(request)
+        # Handle the various store errors
+        except Exception as err:
 
-        scheduler = None
-        lock = None
-        if not isinbox and implicitly:
-            # Get data we need for implicit scheduling
-            calendar = (yield self.iCalendarForUser(request))
-            scheduler = ImplicitScheduler()
-            do_implicit_action, _ignore = (
-                yield scheduler.testImplicitSchedulingDELETE(
-                    request, self, calendar
-                )
-            )
-            if do_implicit_action:
-                lock = MemcacheLock(
-                    "ImplicitUIDLock",
-                    calendar.resourceUID(),
-                    timeout=config.Scheduling.Options.UIDLockTimeoutSeconds,
-                    expire_time=config.Scheduling.Options.UIDLockExpirySeconds,
-                )
+            # Grab the current exception state here so we can use it in a re-raise - we need this because
+            # an inlineCallback might be called and that raises an exception when it returns, wiping out the
+            # original exception "context".
+            ex = Failure()
 
-        try:
-            if lock:
-                yield lock.acquire()
+            if type(err) in CalendarObjectResource.StoreExceptionsStatusErrors:
+                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, str(err)))
 
-            yield super(CalendarObjectResource, self).storeRemove(request, implicitly, where)
+            elif type(err) in CalendarObjectResource.StoreExceptionsErrors:
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    CalendarObjectResource.StoreExceptionsErrors[type(err)],
+                    str(err),
+                ))
+            elif isinstance(err, ValueError):
+                log.err("Error while handling (calendar) PUT: %s" % (e,))
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
+            else:
+                # Return the original failure (exception) state
+                ex.raiseException()
 
-            # Do scheduling
-            if not isinbox and implicitly:
-                yield scheduler.doImplicitScheduling()
 
-        except MemcacheLockTimeoutError:
-            raise HTTPError(StatusResponse(
-                CONFLICT,
-                "Resource: %s currently in use on the server." % (where,))
-            )
+    def storeResource(self, request, parent, destination, destination_uri, destination_parent, hasSource, component, attachmentProcessingDone=False):
+        return StoreCalendarObjectResource(
+            request=request,
+            source=self if hasSource else None,
+            source_uri=request.uri if hasSource else None,
+            sourceparent=parent if hasSource else None,
+            sourcecal=hasSource,
+            deletesource=hasSource,
+            destination=destination,
+            destination_uri=destination_uri,
+            destinationparent=destination_parent,
+            destinationcal=True,
+            calendar=component,
+            attachmentProcessingDone=attachmentProcessingDone,
+        )
 
-        finally:
-            if lock:
-                # Release lock after commit or abort
-                transaction.postCommit(lock.clean)
-                transaction.postAbort(lock.clean)
 
-        returnValue(NO_CONTENT)
-
-
     @requiresPermissions(davxml.WriteContent())
     @inlineCallbacks
     def POST_handler_attachment(self, request, action):
@@ -2820,7 +2863,7 @@
 
 
     @inlineCallbacks
-    def storeRemove(self, request, viaRequest, where):
+    def storeRemove(self, request):
         """
         Delete this collection resource, first deleting each contained
         object resource.
@@ -2834,14 +2877,6 @@
 
         @type request: L{twext.web2.iweb.IRequest}
 
-        @param viaRequest: Indicates if the delete was a direct result of an http_DELETE
-        which for calendars at least will require implicit cancels to be sent.
-
-        @type request: C{bool}
-
-        @param where: the URI at which the resource is being deleted.
-        @type where: C{str}
-
         @return: an HTTP response suitable for sending to a client (or
             including in a multi-status).
 
@@ -2859,9 +2894,7 @@
             ))
 
         response = (
-            yield super(AddressBookCollectionResource, self).storeRemove(
-                request, viaRequest, where
-            )
+            yield super(AddressBookCollectionResource, self).storeRemove(request)
         )
 
         returnValue(response)
@@ -3140,7 +3173,7 @@
             log.debug("Resource not found: %s" % (self,))
             raise HTTPError(NOT_FOUND)
 
-        return self.storeRemove(request, request.uri)
+        return self.storeRemove(request)
 
 
     def http_PROPPATCH(self, request):
@@ -3151,7 +3184,7 @@
 
 
     @inlineCallbacks
-    def storeRemove(self, request, where):
+    def storeRemove(self, request):
         """
         Remove this notification object.
         """
@@ -3169,7 +3202,7 @@
             self._initializeWithObject(None)
 
         except MemcacheLockTimeoutError:
-            raise HTTPError(StatusResponse(CONFLICT, "Resource: %s currently in use on the server." % (where,)))
+            raise HTTPError(StatusResponse(CONFLICT, "Resource: %s currently in use on the server." % (request.uri,)))
         except NoSuchObjectResourceError:
             raise HTTPError(NOT_FOUND)
 

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_resource.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/twistedcaldav/test/test_resource.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -33,34 +33,45 @@
     def qname(self):
         return "StubQnamespace", "StubQname"
 
+
+
 class StubHome(object):
     def properties(self):
         return []
-    
+
+
     def addNotifier(self, notifier):
         pass
 
+
     def nodeName(self):
         return "xyzzy" if self.pushWorking else None
 
+
     def notifierID(self):
         return "xyzzy"
 
+
     def setPushWorking(self, status):
         self.pushWorking = status
 
+
+
 class CalDAVResourceTests(TestCase):
     def setUp(self):
         TestCase.setUp(self)
         self.resource = CalDAVResource()
         self.resource._dead_properties = InMemoryPropertyStore()
 
+
     def test_writeDeadPropertyWritesProperty(self):
         prop = StubProperty()
         self.resource.writeDeadProperty(prop)
         self.assertEquals(self.resource._dead_properties.get(("StubQnamespace", "StubQname")),
                           prop)
 
+
+
 class CommonHomeResourceTests(TestCase):
 
     def test_commonHomeliveProperties(self):
@@ -80,13 +91,13 @@
         self.assertTrue(('http://calendarserver.org/ns/', 'push-transports') in resource.liveProperties())
         self.assertTrue(('http://calendarserver.org/ns/', 'pushkey') in resource.liveProperties())
 
+
     def test_notificationCollectionLiveProperties(self):
         resource = NotificationCollectionResource()
         self.assertTrue(('http://calendarserver.org/ns/', 'getctag') in resource.liveProperties())
 
 
 
-
 class OwnershipTests(TestCase):
     """
     L{CalDAVResource.isOwner} determines if the authenticated principal of the
@@ -170,6 +181,7 @@
         self.assertEquals((yield rsrc.isOwner(request)), True)
 
 
+
 class DefaultAddressBook (TestCase):
 
     def setUp(self):
@@ -177,12 +189,12 @@
         self.createStockDirectoryService()
         self.setupCalendars()
 
+
     @inlineCallbacks
     def test_pick_default_addressbook(self):
         """
         Make calendar
         """
-        
 
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
@@ -206,12 +218,12 @@
 
         request._newStoreTransaction.abort()
 
+
     @inlineCallbacks
     def test_pick_default_other(self):
         """
         Make adbk
         """
-        
 
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
@@ -231,15 +243,15 @@
             HRef("/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk/")
         ))
         request._newStoreTransaction.commit()
-        
+
         # Delete the normal adbk
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
         adbk = yield request.locateResource("/addressbooks/users/wsanchez/addressbook")
-        yield adbk.storeRemove(request, False, "/addressbooks/users/wsanchez/addressbook")
+        yield adbk.storeRemove(request)
 
         home.removeDeadProperty(carddavxml.DefaultAddressBookURL)
-        
+
         # default property not present
         try:
             home.readDeadProperty(carddavxml.DefaultAddressBookURL)
@@ -262,12 +274,12 @@
 
         request._newStoreTransaction.abort()
 
+
     @inlineCallbacks
     def test_fix_shared_default(self):
         """
         Make calendar
         """
-        
 
         request = SimpleRequest(self.site, "GET", "/addressbooks/users/wsanchez/")
         home = yield request.locateResource("/addressbooks/users/wsanchez")
@@ -284,10 +296,10 @@
             self.fail("carddavxml.DefaultAddressBookURL is not present")
         else:
             self.assertEqual(str(default.children[0]), "/addressbooks/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/newadbk/")
-        
+
         # Force the new calendar to think it is a sharee collection
         newadbk._isShareeCollection = True
-        
+
         try:
             default = yield home.readProperty(carddavxml.DefaultAddressBookURL, request)
         except HTTPError:

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -71,7 +71,8 @@
     TooManyAttendeesError, InvalidComponentTypeError, InvalidCalendarAccessError, \
     InvalidUIDError, UIDExistsError, ResourceDeletedError, \
     AttendeeAllowedError, InvalidPerUserDataMerge, ComponentUpdateState, \
-    ValidOrganizerError, ShareeAllowedError, ComponentRemoveState
+    ValidOrganizerError, ShareeAllowedError, ComponentRemoveState, \
+    InvalidComponentForStoreError
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
@@ -87,7 +88,8 @@
 from txdav.common.icommondatastore import IndexedSearchException, \
     InternalDataStoreError, HomeChildNameAlreadyExistsError, \
     HomeChildNameNotAllowedError, ObjectResourceTooBigError, \
-    InvalidObjectResourceError
+    InvalidObjectResourceError, ObjectResourceNameAlreadyExistsError, \
+    ObjectResourceNameNotAllowedError, TooManyObjectResourcesError
 from txdav.xml.rfc2518 import ResourceType
 
 from pycalendar.datetime import PyCalendarDateTime
@@ -884,7 +886,7 @@
         Initialize a calendar pointing at a record in a database.
         """
         super(Calendar, self).__init__(*args, **kw)
-        if self.name() == 'inbox':
+        if self.isInbox():
             self._index = PostgresLegacyInboxIndexEmulator(self)
         else:
             self._index = PostgresLegacyIndexEmulator(self)
@@ -943,14 +945,30 @@
     calendarObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
 
 
+    @inlineCallbacks
     def _createCalendarObjectWithNameInternal(self, name, component, internal_state, options=None):
 
-        if options is None:
-            options = {}
-        options["internal_state"] = internal_state
-        return super(Calendar, self).createObjectResourceWithName(name, component, options)
+        # Create => a new resource name
+        if name in self._objects and self._objects[name]:
+            raise ObjectResourceNameAlreadyExistsError()
 
+        # Apply check to the size of the collection
+        if config.MaxResourcesPerCollection:
+            child_count = (yield self.countObjectResources())
+            if child_count >= config.MaxResourcesPerCollection:
+                raise TooManyObjectResourcesError()
 
+        objectResource = (
+            yield self._objectResourceClass._createInternal(self, name, component, internal_state, options)
+        )
+        self._objects[objectResource.name()] = objectResource
+        self._objects[objectResource.uid()] = objectResource
+
+        # Note: create triggers a notification when the component is set, so we
+        # don't need to call notify() here like we do for object removal.
+        returnValue(objectResource)
+
+
     def calendarObjectsInTimeRange(self, start, end, timeZone):
         raise NotImplementedError()
 
@@ -1337,6 +1355,27 @@
     ]
 
 
+    @classmethod
+    @inlineCallbacks
+    def _createInternal(cls, parent, name, component, internal_state, options=None):
+
+        child = (yield cls.objectWithName(parent, name, None))
+        if child:
+            raise ObjectResourceNameAlreadyExistsError(name)
+
+        if name.startswith("."):
+            raise ObjectResourceNameNotAllowedError(name)
+
+        objectResource = cls(parent, name, None, None, options=options)
+        yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state)
+        yield objectResource._loadPropertyStore(created=True)
+
+        # Note: setComponent triggers a notification, so we don't need to
+        # call notify( ) here like we do for object removal.
+
+        returnValue(objectResource)
+
+
     def _initFromRow(self, row):
         """
         Given a select result using the columns from L{_allColumns}, initialize
@@ -1381,7 +1420,7 @@
         # Do validation on external requests
         if internal_state == ComponentUpdateState.NORMAL:
 
-            # Valid data sizes - do before parsing the data
+            # Valid data sizes
             if config.MaxResourceSize:
                 calsize = len(str(component))
                 if calsize > config.MaxResourceSize:
@@ -1395,7 +1434,7 @@
         if internal_state == ComponentUpdateState.NORMAL:
 
             # Valid calendar data checks
-            yield self.validCalendarDataCheck(component, inserting)
+            self.validCalendarDataCheck(component, inserting)
 
             # Valid calendar component for check
             if not self.calendar().isSupportedComponent(component.mainType()):
@@ -1413,7 +1452,7 @@
 
         # Check access
         if config.EnablePrivateEvents:
-            yield self.validAccess(component, inserting, internal_state)
+            self.validAccess(component, inserting, internal_state)
 
 
     def validIfScheduleMatch(self, etag_match, schedule_tag, internal_state):
@@ -1453,11 +1492,17 @@
 
         try:
             component.validCalendarData(validateRecurrences=self._txn._migrating)
-            component.validCalendarForCalDAV(methodAllowed=self.calendar().name() == 'inbox')
+        except InvalidICalendarDataError, e:
+            raise InvalidObjectResourceError(str(e))
+        try:
+            component.validCalendarForCalDAV(methodAllowed=self.calendar().isInbox())
+        except InvalidICalendarDataError, e:
+            raise InvalidComponentForStoreError(str(e))
+        try:
             if self._txn._migrating:
                 component.validOrganizerForScheduling(doFix=True)
         except InvalidICalendarDataError, e:
-            raise InvalidObjectResourceError(e)
+            raise ValidOrganizerError(str(e))
 
 
     @inlineCallbacks
@@ -1879,22 +1924,22 @@
             # Cannot overwrite a resource with different UID
             if not inserting:
                 if self._uid != new_uid:
-                    raise InvalidUIDError()
+                    raise InvalidUIDError("Cannot change the UID in an existing resource.")
             else:
                 # New UID must be unique for the owner - no need to do this on an overwrite as we can assume
                 # the store is already consistent in this regard
                 elsewhere = (yield self.calendar().ownerHome().hasCalendarResourceUIDSomewhereElse(new_uid, self, "schedule"))
                 if elsewhere:
-                    raise UIDExistsError()
+                    raise UIDExistsError("UID already exists.")
 
 
-    def setComponent(self, component, inserting=False, options=None):
+    def setComponent(self, component, inserting=False):
         """
         Public api for storing a component. This will do full data validation checks on the specified component.
         Scheduling will be done automatically.
         """
 
-        return self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL if options is None else options.get("internal_state"))
+        return self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL)
 
 
     @inlineCallbacks
@@ -2333,8 +2378,10 @@
         returnValue(self._cachedCommponentPerUser[user_uuid])
 
 
-    def remove(self):
-        return self._removeInternal(internal_state=ComponentRemoveState.NORMAL)
+    def remove(self, implicitly=True):
+        return self._removeInternal(
+            internal_state=ComponentRemoveState.NORMAL if implicitly else ComponentRemoveState.NORMAL_NO_IMPLICIT
+        )
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -28,7 +28,7 @@
     InvalidObjectResourceError
 from txdav.caldav.icalendarstore import InvalidComponentTypeError, \
     TooManyAttendeesError, InvalidCalendarAccessError, InvalidUIDError, \
-    UIDExistsError, ComponentUpdateState
+    UIDExistsError, ComponentUpdateState, InvalidComponentForStoreError
 import sys
 from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
 
@@ -215,7 +215,7 @@
             calendar = item
             try:
                 yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
-            except InvalidObjectResourceError:
+            except (InvalidObjectResourceError, InvalidComponentForStoreError):
                 pass
             except:
                 self.fail("Wrong exception raised: %s" % (sys.exc_info()[0].__name__,))

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -106,17 +106,29 @@
     NORMAL -                this is an application layer (user) generated remove that should do all
                             implicit scheduling operations.
 
+    NORMAL_NO_IMPLICIT -    this is an application layer (user) generated remove that deliberately turns
+                            off implicit scheduling operations.
+
     INTERNAL -              remove the resource without implicit scheduling.
     """
 
     NORMAL = NamedConstant()
+    NORMAL_NO_IMPLICIT = NamedConstant()
     INTERNAL = NamedConstant()
 
     NORMAL.description = "normal"
+    NORMAL_NO_IMPLICIT.description = "normal-no-implicit"
     INTERNAL.description = "internal"
 
 
 
+class InvalidComponentForStoreError(CommonStoreError):
+    """
+    Invalid component for an object resource.
+    """
+
+
+
 class InvalidComponentTypeError(CommonStoreError):
     """
     Invalid object resource component type for collection.
@@ -199,8 +211,11 @@
     Specified attachment managed-id is not valid.
     """
 
+    def __str__(self):
+        return "Invalid Managed-ID parameter in calendar data"
 
 
+
 class AttachmentRemoveFailed(Exception):
     """
     Unable to remove an attachment.

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py	2013-04-11 21:14:53 UTC (rev 11031)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py	2013-04-12 18:26:01 UTC (rev 11032)
@@ -4130,7 +4130,7 @@
             raise ObjectResourceNameNotAllowedError(name)
 
         objectResource = cls(parent, name, None, None, options=options)
-        yield objectResource.setComponent(component, inserting=True, options=options)
+        yield objectResource.setComponent(component, inserting=True)
         yield objectResource._loadPropertyStore(created=True)
 
         # Note: setComponent triggers a notification, so we don't need to
@@ -4346,7 +4346,7 @@
                                    resourceID=self._resourceID)
         self.properties()._removeResource()
 
-        self._parentCollection.removedObjectResource(self)
+        yield self._parentCollection.removedObjectResource(self)
 
         # Set to non-existent state
         self._resourceID = None
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130412/73435484/attachment-0001.html>


More information about the calendarserver-changes mailing list