[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