[CalendarServer-changes] [12082] CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav
source_changes at macosforge.org
source_changes at macosforge.org
Wed Mar 12 11:19:14 PDT 2014
Revision: 12082
http://trac.calendarserver.org//changeset/12082
Author: cdaboo at apple.com
Date: 2013-12-12 20:00:05 -0800 (Thu, 12 Dec 2013)
Log Message:
-----------
Checkpoint: more complete cross-pod api calls.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py
CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -1516,6 +1516,7 @@
implements(ICalendarObject)
_objectSchema = schema.CALENDAR_OBJECT
+ _componentClass = VComponent
def __init__(self, calendar, name, uid, resourceID=None, options=None):
@@ -1539,7 +1540,7 @@
@inlineCallbacks
def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
- child = (yield cls.objectWithName(parent, name, None))
+ child = (yield cls.objectWithName(parent, name))
if child:
raise ObjectResourceNameAlreadyExistsError(name)
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -162,6 +162,9 @@
class CalendarObjectExternal(CommonObjectResourceExternal, CalendarObject):
"""
- SQL-based implementation of L{ICalendar}.
+ SQL-based implementation of L{ICalendarObject}.
"""
pass
+
+
+CalendarExternal._objectResourceClass = CalendarObjectExternal
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -1443,6 +1443,8 @@
_objectSchema = schema.ADDRESSBOOK_OBJECT
_bindSchema = schema.SHARED_GROUP_BIND
+ _componentClass = VCard
+
# used by CommonHomeChild._childrenAndMetadataForHomeID() only
# _homeChildSchema = schema.ADDRESSBOOK_OBJECT
# _homeChildMetaDataSchema = schema.ADDRESSBOOK_OBJECT
@@ -1576,10 +1578,6 @@
self._kind = None
self._ownerAddressBookResourceID = None
- # _self._component is the cached, current component
- # super._objectText now contains the text as read of the database only,
- # not including group member text
- self._component = None
self._bindMode = None
self._bindStatus = None
self._bindMessage = None
@@ -1676,7 +1674,8 @@
yield super(AddressBookObject, self).remove()
self._kind = None
self._ownerAddressBookResourceID = None
- self._component = None
+ self._objectText = None
+ self._cachedComponent = None
@inlineCallbacks
@@ -1763,7 +1762,7 @@
@classmethod
@inlineCallbacks
- def _objectWithNameOrID(cls, parent, name, uid, resourceID):
+ def objectWith(cls, parent, name=None, uid=None, resourceID=None):
row, groupBindRow = yield cls._getDBData(parent, name, uid, resourceID)
@@ -2111,7 +2110,7 @@
self._objectText = componentText
self._size = len(self._objectText)
- self._component = component
+ self._cachedComponent = component
self._md5 = hashlib.md5(componentText).hexdigest()
self._componentChanged = originalComponentText != componentText
@@ -2228,7 +2227,7 @@
only allowed in good data.
"""
- if self._component is None:
+ if self._cachedComponent is None:
if self.isGroupForSharedAddressBook():
component = yield self.addressbook()._groupForSharedAddressBookComponent()
@@ -2294,9 +2293,9 @@
component.addProperty(Property("X-ADDRESSBOOKSERVER-KIND", "group"))
component.addProperty(Property("UID", self._uid))
- self._component = component
+ self._cachedComponent = component
- returnValue(self._component)
+ returnValue(self._cachedComponent)
def moveValidation(self, destination, name):
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -76,6 +76,8 @@
class AddressBookObjectExternal(CommonObjectResourceExternal, AddressBookObject):
"""
- SQL-based implementation of L{ICalendar}.
+ SQL-based implementation of L{IAddressBookObject}.
"""
pass
+
+AddressBookExternal._objectResourceClass = AddressBookObjectExternal
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -21,6 +21,7 @@
from txdav.common.datastore.podding.request import ConduitRequest
from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
from txdav.common.icommondatastore import ExternalShareFailed
+from twisted.python.reflect import namedClass
__all__ = [
@@ -375,18 +376,18 @@
# Sharer data access related apis
#
- def _send(self, action, shareeView):
+ def _send(self, action, parent, child=None):
"""
- Base behavior for an operation on a sharee resource.
+ Base behavior for an operation on a L{CommonHomeChild}.
@param shareeView: sharee resource being operated on.
@type shareeView: L{CommonHomeChildExternal}
"""
- homeType = shareeView.ownerHome()._homeType
- ownerUID = shareeView.ownerHome().uid()
- ownerID = shareeView.external_id()
- shareeUID = shareeView.viewerHome().uid()
+ homeType = parent.ownerHome()._homeType
+ ownerUID = parent.ownerHome().uid()
+ ownerID = parent.external_id()
+ shareeUID = parent.viewerHome().uid()
_ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
@@ -397,6 +398,8 @@
"owner_id": ownerID,
"sharee": shareeUID,
}
+ if child is not None:
+ result["resource_id"] = child.id()
return result, recipient
@@ -421,16 +424,24 @@
if ownerHomeChild is None:
FailedCrossPodRequestError("Invalid owner shared resource specified")
- returnValue((ownerHome, ownerHomeChild))
+ resourceID = message.get("resource_id", None)
+ if resourceID is not None:
+ objectResource = yield ownerHomeChild.objectResourceWithID(resourceID)
+ if objectResource is None:
+ FailedCrossPodRequestError("Invalid owner shared object resource specified")
+ else:
+ objectResource = None
+ returnValue((ownerHome, ownerHomeChild, objectResource,))
+
#
# Simple calls are ones where there is no argument and a single return value. We can simplify
# code generation for these by dynamically generating the appropriate class methods.
#
@inlineCallbacks
- def _simple_send(self, actionName, shareeView, args=None, kwargs=None):
+ def _simple_send(self, actionName, shareeView, objectResource=None, args=None, kwargs=None):
"""
A simple send operation that returns a value.
@@ -438,23 +449,28 @@
@type actionName: C{str}
@param shareeView: sharee resource being operated on.
@type shareeView: L{CommonHomeChildExternal}
+ @param objectResource: the resource being operated on, or C{None} for classmethod.
+ @type objectResource: L{CommonObjectResourceExternal}
@param args: list of optional arguments.
@type args: C{list}
@param kwargs: optional keyword arguments.
@type kwargs: C{dict}
"""
- action, recipient = self._send(actionName, shareeView)
+ action, recipient = self._send(actionName, shareeView, objectResource)
if args is not None:
action["arguments"] = args
if kwargs is not None:
action["keywords"] = kwargs
result = yield self.sendRequest(shareeView._txn, recipient, action)
- returnValue(result["value"])
+ if result["result"] == "ok":
+ returnValue(result["value"])
+ elif result["result"] == "exception":
+ raise namedClass(result["class"])(result["message"])
@inlineCallbacks
- def _simple_recv(self, txn, actionName, message, method):
+ def _simple_recv(self, txn, actionName, message, method, onHomeChild=True, transform=None):
"""
A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
and include those only if present.
@@ -465,25 +481,76 @@
@type message: C{dict}
@param method: name of the method to execute on the shared resource to get the result.
@type method: C{str}
+ @param transform: method to call on returned JSON value to convert it to something useful.
+ @type transform: C{callable}
"""
- _ignore_ownerHome, ownerHomeChild = yield self._recv(txn, message, actionName)
- value = yield getattr(ownerHomeChild, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+ _ignore_ownerHome, ownerHomeChild, objectResource = yield self._recv(txn, message, actionName)
+ try:
+ if onHomeChild:
+ # Operate on the L{CommonHomeChild}
+ value = yield getattr(ownerHomeChild, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+ else:
+ # Operate on the L{CommonObjectResource}
+ if objectResource is not None:
+ value = yield getattr(objectResource, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+ else:
+ # classmethod call
+ value = yield getattr(ownerHomeChild._objectResourceClass, method)(ownerHomeChild, *message.get("arguments", ()), **message.get("keywords", {}))
+ except Exception as e:
+ returnValue({
+ "result": "exception",
+ "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+ "message": str(e),
+ })
+ if transform is not None:
+ value = transform(value, ownerHomeChild, objectResource)
+
returnValue({
"result": "ok",
"value": value,
})
+ @staticmethod
+ def _transform_string(value, ownerHomeChild, objectResource):
+ return str(value)
+
+
+ @staticmethod
+ def _transform_externalize(value, ownerHomeChild, objectResource):
+ if isinstance(value, ownerHomeChild._objectResourceClass):
+ value = value.externalize()
+ elif value is not None:
+ value = [v.externalize() for v in value]
+ return value
+
+
@classmethod
- def _make_simple_action(cls, action, method):
- setattr(cls, "send_{}".format(action), lambda self, shareeView, *args, **kwargs: self._simple_send(action, shareeView, args, kwargs))
+ def _make_simple_homechild_action(cls, action, method):
+ setattr(cls, "send_{}".format(action), lambda self, shareeView, *args, **kwargs: self._simple_send(action, shareeView, args=args, kwargs=kwargs))
setattr(cls, "recv_{}".format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method))
-PoddingConduit._make_simple_action("countobjects", "countObjectResources")
-PoddingConduit._make_simple_action("listobjects", "listObjectResources")
-PoddingConduit._make_simple_action("synctoken", "syncToken")
-PoddingConduit._make_simple_action("resourcenamessincerevision", "resourceNamesSinceRevision")
-PoddingConduit._make_simple_action("resourceuidforname", "resourceUIDForName")
-PoddingConduit._make_simple_action("resourcenameforuid", "resourceNameForUID")
+ @classmethod
+ def _make_simple_object_action(cls, action, method, transform_result=None):
+ setattr(cls, "send_{}".format(action), lambda self, shareeView, objectResource, *args, **kwargs: self._simple_send(action, shareeView, objectResource, args=args, kwargs=kwargs))
+ setattr(cls, "recv_{}".format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_result))
+
+
+# Calls on L{CommonHomeChild} objects
+PoddingConduit._make_simple_homechild_action("countobjects", "countObjectResources")
+PoddingConduit._make_simple_homechild_action("listobjects", "listObjectResources")
+PoddingConduit._make_simple_homechild_action("synctoken", "syncToken")
+PoddingConduit._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision")
+PoddingConduit._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
+PoddingConduit._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
+
+# Calls on L{CommonObjectResource} objects
+PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_result=PoddingConduit._transform_externalize)
+PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_result=PoddingConduit._transform_externalize)
+PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_result=PoddingConduit._transform_externalize)
+PoddingConduit._make_simple_object_action("create", "create", transform_result=PoddingConduit._transform_externalize)
+PoddingConduit._make_simple_object_action("setcomponent", "setComponentText")
+PoddingConduit._make_simple_object_action("component", "component", transform_result=PoddingConduit._transform_string)
+PoddingConduit._make_simple_object_action("remove", "remove")
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -29,7 +29,9 @@
FakeConduitRequest
from txdav.common.datastore.sql_tables import _BIND_STATUS_ACCEPTED
from pycalendar.datetime import DateTime
-from twistedcaldav.ical import Component
+from twistedcaldav.ical import Component, normalize_iCalStr
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
+ ObjectResourceNameNotAllowedError
class TestConduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
@@ -208,12 +210,28 @@
END:VCALENDAR
""".replace("\n", "\r\n").format(**nowYear)
+ caldata1_changed = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T150000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance changed
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
caldata2 = """BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
-UID:ui2
+UID:uid2
DTSTART:{now:04d}0102T160000Z
DURATION:PT1H
CREATED:20060102T190000Z
@@ -224,6 +242,22 @@
END:VCALENDAR
""".replace("\n", "\r\n").format(**nowYear)
+ caldata3 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid3
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
@inlineCallbacks
def test_basic_share(self):
"""
@@ -493,3 +527,360 @@
uid = yield shared.resourceNameForUID("uid2")
self.assertTrue(uid is None)
yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_loadallobjects(self):
+ """
+ Test that action=loadallobjects works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id1 = resource1.id()
+ resource2 = yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+ resource_id2 = resource2.id()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResources()
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 2)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "2.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid2",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id2,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithName("2.ics")
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is byuid["uid1"])
+ resource = yield shared.objectResourceWithUID("uid2")
+ self.assertTrue(resource is byuid["uid2"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithID(resource_id2)
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResources()
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 1)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("2.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid2",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id2,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithName("2.ics")
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid2")
+ self.assertTrue(resource is byuid["uid2"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id2)
+ self.assertTrue(resource is byname["2.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_loadallobjectswithnames(self):
+ """
+ Test that action=loadallobjectswithnames works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id1 = resource1.id()
+ yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+ resource3 = yield calendar1.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3))
+ resource_id3 = resource3.id()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResources()
+ self.assertEqual(len(resources), 3)
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 2)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("1.ics", "3.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid1", "uid3",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id1, resource_id3,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithName("3.ics")
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is byuid["uid1"])
+ resource = yield shared.objectResourceWithUID("uid3")
+ self.assertTrue(resource is byuid["uid3"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is byname["1.ics"])
+ resource = yield shared.objectResourceWithID(resource_id3)
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",))
+ byname = dict([(resource.name(), resource) for resource in resources])
+ byuid = dict([(resource.uid(), resource) for resource in resources])
+ self.assertEqual(len(resources), 1)
+ self.assertEqual(set([resource.name() for resource in resources]), set(("3.ics",)))
+ self.assertEqual(set([resource.uid() for resource in resources]), set(("uid3",)))
+ self.assertEqual(set([resource.id() for resource in resources]), set((resource_id3,)))
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithName("3.ics")
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithName("Missing.ics")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithUID("uid3")
+ self.assertTrue(resource is byuid["uid3"])
+ resource = yield shared.objectResourceWithUID("uid-missing")
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id1)
+ self.assertTrue(resource is None)
+ resource = yield shared.objectResourceWithID(resource_id3)
+ self.assertTrue(resource is byname["3.ics"])
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_objectwith(self):
+ """
+ Test that action=objectwith works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ resource = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id = resource.id()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+
+ resource = yield shared.objectResourceWithName("2.ics")
+ self.assertTrue(resource is None)
+
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+
+ resource = yield shared.objectResourceWithUID("uid2")
+ self.assertTrue(resource is None)
+
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithID(resource_id)
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+
+ resource = yield shared.objectResourceWithID(0)
+ self.assertTrue(resource is None)
+
+ yield self.otherCommit()
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ yield object1.remove()
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithName("1.ics")
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithID(resource_id)
+ self.assertTrue(resource is None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_create(self):
+ """
+ Test that action=create works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ resource_id = resource.id()
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+ yield self.otherCommit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ resource = yield shared.objectResourceWithUID("uid1")
+ self.assertTrue(resource is not None)
+ self.assertEqual(resource.name(), "1.ics")
+ self.assertEqual(resource.uid(), "uid1")
+ self.assertEqual(resource.id(), resource_id)
+ yield self.otherCommit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ self.assertTrue(object1 is not None)
+ self.assertEqual(object1.name(), "1.ics")
+ self.assertEqual(object1.uid(), "uid1")
+ self.assertEqual(object1.id(), resource_id)
+ yield self.commit()
+
+
+ @inlineCallbacks
+ def test_create_exception(self):
+ """
+ Test that action=create fails when a duplicate name is used.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ yield self.failUnlessFailure(shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError)
+ yield self.otherAbort()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+ yield self.failUnlessFailure(shared.createCalendarObjectWithName(".2.ics", Component.fromString(self.caldata2)), ObjectResourceNameNotAllowedError)
+ yield self.otherAbort()
+
+
+ @inlineCallbacks
+ def test_setcomponent(self):
+ """
+ Test that action=setcomponent works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ ical = yield shared_object.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
+ yield self.otherCommit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ changed = yield shared_object.setComponent(Component.fromString(self.caldata1_changed))
+ self.assertFalse(changed)
+ ical = yield shared_object.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
+ yield self.otherCommit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ ical = yield object1.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed))
+ yield self.commit()
+
+
+ @inlineCallbacks
+ def test_component(self):
+ """
+ Test that action=component works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ ical = yield shared_object.component()
+ self.assertTrue(isinstance(ical, Component))
+ self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1))
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_remove(self):
+ """
+ Test that action=create works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ yield shared_object.remove()
+ yield self.otherCommit()
+
+ shared_object = yield self.calendarObjectUnderTest(txn=self.newOtherTransaction(), home="puser01", calendar_name="shared-calendar", name="1.ics")
+ self.assertTrue(shared_object is None)
+ yield self.otherCommit()
+
+ object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+ self.assertTrue(object1 is None)
+ yield self.commit()
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -4102,33 +4102,39 @@
@classmethod
def objectWithName(cls, home, name, accepted=True):
- return cls._objectWithNameOrID(home, name=name, accepted=accepted)
+ return cls.objectWith(home, name=name, accepted=accepted)
@classmethod
def objectWithID(cls, home, resourceID, accepted=True):
- return cls._objectWithNameOrID(home, resourceID=resourceID, accepted=accepted)
+ return cls.objectWith(home, resourceID=resourceID, accepted=accepted)
@classmethod
def objectWithExternalID(cls, home, externalID, accepted=True):
- return cls._objectWithNameOrID(home, externalID=externalID, accepted=accepted)
+ return cls.objectWith(home, externalID=externalID, accepted=accepted)
@classmethod
@inlineCallbacks
- def _objectWithNameOrID(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
- # replaces objectWithName()
+ def objectWith(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
"""
- Retrieve the child with the given C{name} or C{resourceID} contained in the given
- C{home}.
+ Create the object using one of the specified arguments as the key to load it. One
+ and only one of the keyword arguments must be set.
- @param home: a L{CommonHome}.
+ @param parent: parent collection
+ @type parent: L{CommonHomeChild}
+ @param name: name of the resource, or C{None}
+ @type name: C{str}
+ @param uid: resource data UID, or C{None}
+ @type uid: C{str}
+ @param resourceID: resource id
+ @type resourceID: C{int}
+ @param accepted: if C{True} only load owned or accepted share items
+ @type accepted: C{bool}
- @param name: a string; the name of the L{CommonHomeChild} to retrieve.
-
- @return: an L{CommonHomeChild} or C{None} if no such child
- exists.
+ @return: the new object or C{None} if not found
+ @rtype: C{CommonHomeChild}
"""
dbData = yield cls._getDBData(home, name, resourceID, externalID)
@@ -4376,6 +4382,7 @@
for result in results:
self._objects[result.name()] = result
self._objects[result.uid()] = result
+ self._objects[result.id()] = result
self._objectNames = sorted([result.name() for result in results])
returnValue(results)
@@ -4389,6 +4396,7 @@
for result in results:
self._objects[result.name()] = result
self._objects[result.uid()] = result
+ self._objects[result.id()] = result
self._objectNames = sorted([result.name() for result in results])
returnValue(results)
@@ -4457,18 +4465,13 @@
We create the empty object first then have it initialize itself from the
store.
"""
- if resourceID:
- objectResource = (
- yield self._objectResourceClass.objectWithID(self, resourceID)
- )
- else:
- objectResource = (
- yield self._objectResourceClass.objectWithName(self, name, uid)
- )
+ objectResource = (
+ yield self._objectResourceClass.objectWith(self, name=name, uid=uid, resourceID=resourceID)
+ )
if objectResource:
self._objects[objectResource.name()] = objectResource
self._objects[objectResource.uid()] = objectResource
- self._objects[objectResource._resourceID] = objectResource
+ self._objects[objectResource.id()] = objectResource
else:
if resourceID:
self._objects[resourceID] = None
@@ -4558,6 +4561,7 @@
)
self._objects[objectResource.name()] = objectResource
self._objects[objectResource.uid()] = objectResource
+ self._objects[objectResource.id()] = 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.
@@ -4568,6 +4572,7 @@
def removedObjectResource(self, child):
self._objects.pop(child.name(), None)
self._objects.pop(child.uid(), None)
+ self._objects.pop(child.id(), None)
if self._objectNames and child.name() in self._objectNames:
self._objectNames.remove(child.name())
yield self._deleteRevision(child.name())
@@ -4639,7 +4644,7 @@
# Clean this collections cache and signal sync change
self._objects.pop(name, None)
self._objects.pop(uid, None)
- self._objects.pop(child._resourceID, None)
+ self._objects.pop(child.id(), None)
yield self._deleteRevision(name)
yield self.notifyChanged()
@@ -4670,7 +4675,7 @@
# Signal sync change on new collection
newparent._objects.pop(name, None)
newparent._objects.pop(uid, None)
- newparent._objects.pop(child._resourceID, None)
+ newparent._objects.pop(child.id(), None)
yield newparent._insertRevision(newname)
yield newparent.notifyChanged()
@@ -4876,6 +4881,7 @@
_externalClass = None
_objectSchema = None
+ _componentClass = None
BATCH_LOAD_SIZE = 50
@@ -4961,7 +4967,8 @@
self._size = None
self._created = None
self._modified = None
- self._notificationData = None
+ self._textData = None
+ self._cachedComponent = None
self._locked = False
@@ -5023,7 +5030,9 @@
@inlineCallbacks
def loadAllObjectsWithNames(cls, parent, names):
"""
- Load all child objects with the specified names, doing so in batches.
+ Load all child objects with the specified names, doing so in batches (because we need to match
+ using SQL "resource_name in (...)" where there might be a character length limit on the number
+ of items in the set).
"""
names = tuple(names)
results = []
@@ -5062,7 +5071,7 @@
# Optimize case of single name to load
if len(names) == 1:
- obj = yield cls.objectWithName(parent, names[0], None)
+ obj = yield cls.objectWithName(parent, names[0])
returnValue([obj] if obj else [])
results = []
@@ -5093,19 +5102,40 @@
@classmethod
- def objectWithName(cls, parent, name, uid):
- return cls._objectWithNameOrID(parent, name, uid, None)
+ def objectWithName(cls, parent, name):
+ return cls.objectWith(parent, name=name)
@classmethod
+ def objectWithUID(cls, parent, uid):
+ return cls.objectWith(parent, uid=uid)
+
+
+ @classmethod
def objectWithID(cls, parent, resourceID):
- return cls._objectWithNameOrID(parent, None, None, resourceID)
+ return cls.objectWith(parent, resourceID=resourceID)
@classmethod
@inlineCallbacks
- def _objectWithNameOrID(cls, parent, name, uid, resourceID):
+ def objectWith(cls, parent, name=None, uid=None, resourceID=None):
+ """
+ Create the object using one of the specified arguments as the key to load it. One
+ and only one of the keyword arguments must be set.
+ @param parent: parent collection
+ @type parent: L{CommonHomeChild}
+ @param name: name of the resource, or C{None}
+ @type name: C{str}
+ @param uid: resource data UID, or C{None}
+ @type uid: C{str}
+ @param resourceID: resource id
+ @type resourceID: C{int}
+
+ @return: the new object or C{None} if not found
+ @rtype: C{CommonObjectResource}
+ """
+
row = yield cls._getDBData(parent, name, uid, resourceID)
if row:
@@ -5195,6 +5225,27 @@
)
+ def externalize(self):
+ """
+ Create a dictionary mapping key attributes so this object can be sent over a cross-pod call
+ and reconstituted at the other end. Note that the other end may have a different schema so
+ the attributes may not match exactly and will need to be processed accordingly.
+ """
+ return dict([(attr[1:], getattr(self, attr)) for attr in self._rowAttributes()])
+
+
+ @classmethod
+ def internalize(cls, mapping):
+ """
+ Given a mapping generated by L{externalize}, convert the values into an array of database
+ like items that conforms to the ordering of L{_allColumns} so it can be fed into L{makeClass}.
+ Note that there may be a schema mismatch with the external data, so treat missing items as
+ C{None} and ignore extra items.
+ """
+
+ return [mapping.get(row[1:]) for row in cls._rowAttributes()]
+
+
@inlineCallbacks
def _loadPropertyStore(self, props=None, created=False):
if props is None:
@@ -5309,6 +5360,14 @@
raise NotImplementedError
+ def setComponentText(self, component_text, inserting=False, options=None):
+ """
+ This api is needed for cross-pod calls where the component is serialized as a str and we need
+ to convert it back to the actual component class.
+ """
+ return self.setComponent(self._componentClass.fromString(component_text), inserting, options)
+
+
def component(self):
raise NotImplementedError
@@ -5361,7 +5420,8 @@
self._size = None
self._created = None
self._modified = None
- self._notificationData = None
+ self._textData = None
+ self._cachedComponent = None
def removeNotifyCategory(self):
@@ -5417,19 +5477,19 @@
@inlineCallbacks
def _text(self):
- if self._notificationData is None:
+ if self._textData is None:
texts = (
yield self._textByIDQuery.on(self._txn,
resourceID=self._resourceID)
)
if texts:
text = texts[0][0]
- self._notificationData = text
+ self._textData = text
returnValue(text)
else:
raise ConcurrentModification()
else:
- returnValue(self._notificationData)
+ returnValue(self._textData)
Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py 2013-12-13 03:03:31 UTC (rev 12081)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py 2013-12-13 04:00:05 UTC (rev 12082)
@@ -225,16 +225,6 @@
@inlineCallbacks
- def objectResources(self):
- raise NotImplementedError("TODO: external resource")
-
-
- @inlineCallbacks
- def objectResourcesWithNames(self, names):
- raise NotImplementedError("TODO: external resource")
-
-
- @inlineCallbacks
def listObjectResources(self):
if self._objectNames is None:
try:
@@ -258,18 +248,6 @@
returnValue(len(self._objectNames))
- def objectResourceWithName(self, name):
- raise NotImplementedError("TODO: external resource")
-
-
- def objectResourceWithUID(self, uid):
- raise NotImplementedError("TODO: external resource")
-
-
- def objectResourceWithID(self, resourceID):
- raise NotImplementedError("TODO: external resource")
-
-
@inlineCallbacks
def resourceNameForUID(self, uid):
try:
@@ -314,11 +292,21 @@
@inlineCallbacks
def createObjectResourceWithName(self, name, component, options=None):
+ """
+ Actually I think we can defer this to the object resource class's .create()
+ """
raise NotImplementedError("TODO: external resource")
@inlineCallbacks
def moveObjectResource(self, child, newparent, newname=None):
+ """
+ The base class does an optimization to avoid removing/re-creating
+ the actual object resource data. That might not always be possible
+ with external shares if the shared resource is moved to a collection
+ that is not shared or shared by someone else on a different (third)
+ pod. The best bet here is to treat the move as a delete/create.
+ """
raise NotImplementedError("TODO: external resource")
@@ -351,4 +339,81 @@
A CommonObjectResource for a resource not hosted on this system, but on another pod. This will forward
specific apis to the other pod using cross-pod requests.
"""
- pass
+
+ @classmethod
+ @inlineCallbacks
+ def loadAllObjects(cls, parent):
+ mapping_list = yield parent._txn.store().conduit.send_loadallobjects(parent, None)
+
+ results = []
+ if mapping_list:
+ for mapping in mapping_list:
+ child = yield cls.makeClass(parent, cls.internalize(mapping))
+ results.append(child)
+ returnValue(results)
+
+
+ @classmethod
+ @inlineCallbacks
+ def loadAllObjectsWithNames(cls, parent, names):
+ mapping_list = yield parent._txn.store().conduit.send_loadallobjectswithnames(parent, None, names)
+
+ results = []
+ if mapping_list:
+ for mapping in mapping_list:
+ child = yield cls.makeClass(parent, cls.internalize(mapping))
+ results.append(child)
+ returnValue(results)
+
+
+ @classmethod
+ @inlineCallbacks
+ def objectWith(cls, parent, name=None, uid=None, resourceID=None):
+ mapping = yield parent._txn.store().conduit.send_objectwith(parent, None, name, uid, resourceID)
+
+ if mapping:
+ child = yield cls.makeClass(parent, cls.internalize(mapping))
+ returnValue(child)
+ else:
+ returnValue(None)
+
+
+ @classmethod
+ @inlineCallbacks
+ def create(cls, parent, name, component, options=None):
+ mapping = yield parent._txn.store().conduit.send_create(parent, None, name, component, options=options)
+
+ if mapping:
+ child = yield cls.makeClass(parent, cls.internalize(mapping))
+ returnValue(child)
+ else:
+ returnValue(None)
+
+
+ @inlineCallbacks
+ def setComponent(self, component, inserting=False, options=None):
+ changed = yield self._txn.store().conduit.send_setcomponent(self.parentCollection(), self, str(component), inserting, options)
+ self._cachedComponent = None
+ returnValue(changed)
+
+
+ @inlineCallbacks
+ def component(self):
+ if self._cachedComponent is None:
+ text = yield self._txn.store().conduit.send_component(self.parentCollection(), self)
+ self._cachedComponent = self._componentClass.fromString(text)
+
+ returnValue(self._cachedComponent)
+
+
+ @inlineCallbacks
+ def moveTo(self, destination, name=None):
+ """
+ Probably OK to leave this to the base implementation which calls up to the parent after some validation.
+ """
+ raise NotImplementedError
+
+
+ @inlineCallbacks
+ def remove(self):
+ yield self._txn.store().conduit.send_remove(self.parentCollection(), self)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/80026a16/attachment.html>
More information about the calendarserver-changes
mailing list