[CalendarServer-changes] [12111] CalendarServer/branches/users/cdaboo/cross-pod-sharing

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:17:54 PDT 2014


Revision: 12111
          http://trac.calendarserver.org//changeset/12111
Author:   cdaboo at apple.com
Date:     2013-12-16 09:12:05 -0800 (Mon, 16 Dec 2013)
Log Message:
-----------
Checkpoint: fixes for freebusy and sync, plus other bits. Working through complete set of cross-pod CDT items. More to come.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/resource.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/sharing.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/freebusy.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/ischedule/xml.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_sharing.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql_sharing.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/resource.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/podding/test/util.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

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/resource.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/resource.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -2667,10 +2667,9 @@
     @inlineCallbacks
     def _indexWhatChanged(self, revision, depth):
         # The newstore implementation supports this directly
-        changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
+        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
             revision, depth
         )
-        notallowed = []
 
         # Need to insert some addition items on first sync
         if revision == 0:
@@ -2894,10 +2893,9 @@
     @inlineCallbacks
     def _indexWhatChanged(self, revision, depth):
         # The newstore implementation supports this directly
-        changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
+        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
             revision, depth
         )
-        notallowed = []
 
         # Need to insert some addition items on first sync
         if revision == 0:

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/sharing.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/sharing.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -805,6 +805,12 @@
 
         # Accept the share
         shareeView = yield self._newStoreHome.acceptShare(inviteUID, summary)
+        if shareeView is None:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "invalid-share"),
+                "Invite UID not valid",
+            ))
 
         # Return the URL of the shared collection
         sharedAsURL = joinURL(self.url(), shareeView.shareName())
@@ -820,7 +826,13 @@
     def declineShare(self, request, inviteUID):
 
         # Remove it if it is in the DB
-        yield self._newStoreHome.declineShare(inviteUID)
+        result = yield self._newStoreHome.declineShare(inviteUID)
+        if not result:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, "invalid-share"),
+                "Invite UID not valid",
+            ))
         returnValue(Response(code=responsecode.NO_CONTENT))
 
 

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/storebridge.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/storebridge.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -462,6 +462,8 @@
         if self.isShareeResource():
             log.debug("Removing shared collection %s" % (self,))
             yield self.removeShareeResource(request)
+            # Re-initialize to get stuff setup again now we have no object
+            self._initializeWithHomeChild(None, self._parentResource)
             returnValue(NO_CONTENT)
 
         log.debug("Deleting collection %s" % (self,))
@@ -3371,6 +3373,8 @@
         if self.isShareeResource():
             log.debug("Removing shared resource %s" % (self,))
             yield self.removeShareeResource(request)
+            # Re-initialize to get stuff setup again now we have no object
+            self._initializeWithObject(None, self._newStoreParent)
             returnValue(NO_CONTENT)
         elif self._newStoreObject.isGroupForSharedAddressBook():
             abCollectionResource = (yield request.locateResource(parentForURL(request.uri)))

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/freebusy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/freebusy.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/freebusy.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -92,7 +92,6 @@
 
 
 
- at inlineCallbacks
 def generateFreeBusyInfo(
     calresource,
     fbinfo,
@@ -107,6 +106,86 @@
     logItems=None,
 ):
     """
+    Get freebusy information for a calendar. Different behavior for internal vs external calendars.
+
+    See L{_internalGenerateFreeBusyInfo} for argument description.
+    """
+
+    # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
+    # can be split across L{CalendarObject} and L{CalendarObjectExternal}
+    if calresource.external():
+        return _externalGenerateFreeBusyInfo(
+            calresource,
+            fbinfo,
+            timerange,
+            matchtotal,
+            excludeuid,
+            organizer,
+            organizerPrincipal,
+            same_calendar_user,
+            servertoserver,
+            event_details,
+            logItems
+        )
+    else:
+        return _internalGenerateFreeBusyInfo(
+            calresource,
+            fbinfo,
+            timerange,
+            matchtotal,
+            excludeuid,
+            organizer,
+            organizerPrincipal,
+            same_calendar_user,
+            servertoserver,
+            event_details,
+            logItems
+        )
+
+
+
+ at inlineCallbacks
+def _externalGenerateFreeBusyInfo(
+    calresource,
+    fbinfo,
+    timerange,
+    matchtotal,
+    excludeuid=None,
+    organizer=None,
+    organizerPrincipal=None,
+    same_calendar_user=False,
+    servertoserver=False,
+    event_details=None,
+    logItems=None,
+):
+    """
+    Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
+    any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
+
+    See L{_internalGenerateFreeBusyInfo} for argument description.
+    """
+    fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid, organizer, organizerPrincipal, same_calendar_user, servertoserver, event_details)
+    for i in range(3):
+        fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
+    returnValue(matchtotal)
+
+
+
+ at inlineCallbacks
+def _internalGenerateFreeBusyInfo(
+    calresource,
+    fbinfo,
+    timerange,
+    matchtotal,
+    excludeuid=None,
+    organizer=None,
+    organizerPrincipal=None,
+    same_calendar_user=False,
+    servertoserver=False,
+    event_details=None,
+    logItems=None,
+):
+    """
     Run a free busy report on the specified calendar collection
     accumulating the free busy info for later processing.
     @param calresource: the L{Calendar} for a calendar collection.

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/ischedule/xml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/ischedule/xml.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/ischedule/xml.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -259,14 +259,18 @@
 
 
     @classmethod
-    def fromCalendar(clazz, calendar):
+    def fromCalendar(clazz, calendar, format=None):
+        attrs = {}
+        if format is not None and format != "text/calendar":
+            attrs["content-type"] = format
+
         if isinstance(calendar, str):
             if not calendar:
                 raise ValueError("Missing calendar data")
             return clazz(PCDATAElement(calendar))
         elif isinstance(calendar, iComponent):
             assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
-            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
+            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=format)))
         else:
             raise ValueError("Not a calendar: %s" % (calendar,))
 

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-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -2147,6 +2147,8 @@
         Scheduling will be done automatically.
         """
 
+        if isinstance(component, str) or isinstance(component, unicode):
+            component = self._componentClass.fromString(component)
         return self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL, smart_merge)
 
 

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -1602,7 +1602,7 @@
 
         home = yield self.homeUnderTest()
 
-        changed, deleted = yield home.resourceNamesSinceToken(
+        changed, deleted, invalid = yield home.resourceNamesSinceToken(
             self.token2revision(st), "infinity")
 
         self.assertEquals(set(changed), set(["calendar_1/",
@@ -1610,11 +1610,13 @@
                                              "calendar_1/2.ics",
                                              "other-calendar/"]))
         self.assertEquals(set(deleted), set(["calendar_1/2.ics"]))
+        self.assertEquals(invalid, [])
 
-        changed, deleted = yield home.resourceNamesSinceToken(
+        changed, deleted, invalid = yield home.resourceNamesSinceToken(
             self.token2revision(st2), "infinity")
         self.assertEquals(changed, [])
         self.assertEquals(deleted, [])
+        self.assertEquals(invalid, [])
 
 
     @inlineCallbacks

Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -0,0 +1,680 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from twisted.internet.defer import inlineCallbacks
+
+from twext.python.clsprop import classproperty
+from txdav.common.datastore.test.util import populateCalendarsFrom
+from txdav.common.datastore.sql_tables import _BIND_MODE_READ, \
+    _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
+
+
+class BaseSharingTests(MultiStoreConduitTest):
+
+    """
+    Test store-based calendar sharing.
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(BaseSharingTests, self).setUp()
+        yield self.populate()
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+    cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:20131122T140000
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+"""
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        return {
+        "user01": {
+            "calendar": {
+                "cal1.ics": (cls.cal1, None,),
+            },
+            "inbox": {
+            },
+        },
+        "user02": {
+            "calendar": {
+            },
+            "inbox": {
+            },
+        },
+        "user03": {
+            "calendar": {
+            },
+            "inbox": {
+            },
+        },
+    }
+
+
+
+class CalendarSharing(BaseSharingTests):
+
+    @inlineCallbacks
+    def test_no_shares(self):
+        """
+        Test that initially there are no shares.
+        """
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_invite_sharee(self):
+        """
+        Test invite/uninvite creates/removes shares and notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertEqual(invites[0].uid, shareeView.shareUID())
+        self.assertEqual(invites[0].ownerUID, "user01")
+        self.assertEqual(invites[0].shareeUID, "puser02")
+        self.assertEqual(invites[0].mode, _BIND_MODE_READ)
+        self.assertEqual(invites[0].status, _BIND_STATUS_INVITED)
+        self.assertEqual(invites[0].summary, "summary")
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID, ])
+        yield self.otherCommit()
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        yield calendar.uninviteUserFromShare("puser02")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [])
+        yield self.otherCommit()
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+        yield calendar.setShared(False)
+        self.assertFalse(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_share(self):
+        """
+        Test that invite+accept creates shares and notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Re-accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_decline_share(self):
+        """
+        Test that invite+decline does not create shares but does create notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Decline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Redecline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_decline_share(self):
+        """
+        Test that invite+accept/decline creates/removes shares and notifications.
+        Decline via the home.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        inviteUID = shareeView.shareUID()
+
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Decline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_remove_share(self):
+        """
+        Test that invite+accept/decline creates/removes shares and notifications.
+        Decline via the shared collection (removal).
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID("puser02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+        yield self.commit()
+
+        # Delete
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        yield shared.deleteShare()
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+
+    @inlineCallbacks
+    def test_accept_remove_accept(self):
+        yield self.createShare()
+        yield self.removeShare()
+        shared_name = yield self.createShare()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_accept_remove_accept_newcalendar(self):
+        """
+        Test that deleting and re-creating a share with the same sharer name works.
+        """
+
+        home = yield self.homeUnderTest(name="user01", create=True)
+        yield home.createCalendarWithName("shared")
+        yield self.commit()
+
+        shared_name = yield self.createShare(name="shared")
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+        yield self.removeShare(name="shared")
+        home = yield self.homeUnderTest(name="user01", create=True)
+        yield home.removeCalendarWithName("shared")
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is None)
+        yield self.otherCommit()
+
+        home = yield self.homeUnderTest(name="user01", create=True)
+        yield home.createCalendarWithName("shared")
+        yield self.commit()
+
+        shared_name = yield self.createShare(name="shared")
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_inviteProperties(self):
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield calendar.setUsedForFreeBusy(True)
+        yield self.commit()
+
+        shared_name = yield self.createShare()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+        self.assertFalse(shared.isUsedForFreeBusy())
+
+
+    @inlineCallbacks
+    def test_direct_sharee(self):
+        """
+        Test invite/uninvite creates/removes shares and notifications.
+        """
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.directShareWithUser("puser02")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertEqual(invites[0].uid, shareeView.shareUID())
+        self.assertEqual(invites[0].ownerUID, "user01")
+        self.assertEqual(invites[0].shareeUID, "puser02")
+        self.assertEqual(invites[0].mode, _BIND_MODE_DIRECT)
+        self.assertEqual(invites[0].status, _BIND_STATUS_ACCEPTED)
+
+        sharedName = shareeView.name()
+
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="user02", name=sharedName)
+        self.assertTrue(shared is not None)
+
+        notifyHome = yield txn2.notificationsWithUID("user02")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 0)
+        yield self.otherCommit()
+
+        # Remove
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        yield shared.deleteShare()
+        yield self.otherCommit()
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 0)
+
+    test_direct_sharee.skip = True
+
+    @inlineCallbacks
+    def test_sharedNotifierID(self):
+        shared_name = yield self.createShare()
+
+        home = yield self.homeUnderTest(name="user01")
+        self.assertEquals(home.notifierID(), ("CalDAV", "user01",))
+        calendar = yield home.calendarWithName("calendar")
+        self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        home = yield self.homeUnderTest(txn=txn2, name="puser02")
+        self.assertEquals(home.notifierID(), ("CalDAV", "puser02",))
+        calendar = yield home.calendarWithName(shared_name)
+        self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+
+
+    @inlineCallbacks
+    def test_sharedWithTwo(self):
+        shared_name1 = yield self.createShare(shareeGUID="puser02")
+        shared_name2 = yield self.createShare(shareeGUID="puser03")
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name1)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser03", name=shared_name2)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+
+class SharingRevisions(BaseSharingTests):
+    """
+    Test store-based sharing and interaction with revision table.
+    """
+
+    @inlineCallbacks
+    def test_shareWithRevision(self):
+        """
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        """
+        sharedName = yield self.createShare()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_updateShareRevision(self):
+        """
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        """
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+        newCalName = shareeView.shareUID()
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        otherCal = yield otherHome.anyObjectWithShareUID(newCalName)
+        self.assertEqual(otherCal._bindRevision, 0)
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        shareeView = yield shareeHome.acceptShare(newCalName)
+        sharedName = shareeView.name()
+        yield self.otherCommit()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_sharedRevisions(self):
+        """
+        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+        """
+        sharedName = yield self.createShare()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+        otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+        sync_token = yield otherCal.syncToken()
+        revision = otherCal.revisionFromToken(sync_token)
+
+        changed, deleted = yield otherCal.resourceNamesSinceRevision(revision - 1)
+        self.assertNotEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        changed, deleted = yield otherCal.resourceNamesSinceRevision(revision)
+        self.assertEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        sync_token = yield otherHome.syncToken()
+        revision = otherHome.revisionFromToken(sync_token)
+
+        for depth in ("1", "infinity",):
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision - 1, depth)
+            self.assertEqual(len(changed), 0 if depth == "infinity" else 1)
+            self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+        yield self.otherCommit()
+
+        yield self.removeShare()
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+
+        for depth in ("1", "infinity",):
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 1)
+            self.assertEqual(len(invalid), 0)

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_sharing.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_sharing.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -563,10 +563,12 @@
         self.assertEqual(len(deleted), 0)
 
         for depth in ("1", "infinity",):
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision - 1, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision - 1, depth)
             self.assertNotEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
             self.assertEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)

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-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -1956,6 +1956,9 @@
     @inlineCallbacks
     def setComponent(self, component, inserting=False):
 
+        if isinstance(component, str) or isinstance(component, unicode):
+            component = self._componentClass.fromString(component)
+
         self._componentChanged = False
 
         # Handle all validation operations here.

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql_sharing.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql_sharing.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -1144,13 +1144,15 @@
 
         otherHome = yield self.addressbookHomeUnderTest(name="user02")
         for depth in ("1", "infinity",):
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
             self.assertNotEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
             self.assertEqual(len(changed), 0)
             self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 0)
 
 
     @inlineCallbacks

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-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -22,6 +22,8 @@
 from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
 from txdav.common.icommondatastore import ExternalShareFailed
 from twisted.python.reflect import namedClass
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from twistedcaldav.caldavxml import TimeRange
 
 
 __all__ = [
@@ -372,6 +374,66 @@
         })
 
 
+    @inlineCallbacks
+    def send_shareremove(self, txn, homeType, ownerUID, shareeUID, shareUID):
+        """
+        Send a sharing remove cross-pod message.
+
+        @param homeType: Type of home being shared.
+        @type homeType: C{int}
+        @param ownerUID: GUID of the sharer.
+        @type ownerUID: C{str}
+        @param shareeUID: GUID of the recipient
+        @type shareeUID: C{str}
+        @param shareUID: Resource/invite ID for recipient
+        @type shareUID: C{str}
+        """
+
+        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+        action = {
+            "action": "shareremove",
+            "type": homeType,
+            "owner": ownerUID,
+            "sharee": shareeUID,
+            "share_id": shareUID,
+        }
+
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_shareremove(self, txn, message):
+        """
+        Process a sharing remove cross-pod message. Message arguments as per L{send_shareremove}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        if message["action"] != "shareremove":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareremove".format(message["action"]))
+
+        # Create a share
+        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
+        if ownerHome is None or ownerHome.external():
+            FailedCrossPodRequestError("Invalid owner UID specified")
+
+        try:
+            yield ownerHome.processExternalRemove(
+                message["owner"],
+                message["sharee"],
+                message["share_id"],
+            )
+        except ExternalShareFailed as e:
+            FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            "result": "ok",
+        })
+
+
     #
     # Sharer data access related apis
     #
@@ -415,24 +477,28 @@
         if message["action"] != expected_action:
             raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
 
-        # Create a share
-        ownerHome = yield txn.homeWithUID(message["type"], message["owner"], create=True)
+        # Get a share
+        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
         if ownerHome is None or ownerHome.external():
             FailedCrossPodRequestError("Invalid owner UID specified")
 
-        ownerHomeChild = yield ownerHome.childWithID(message["owner_id"])
-        if ownerHomeChild is None:
-            FailedCrossPodRequestError("Invalid owner shared resource specified")
+        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"])
+        if shareeHome is None or not shareeHome.external():
+            FailedCrossPodRequestError("Invalid sharee UID specified")
 
+        shareeView = yield shareeHome.childWithID(message["owner_id"])
+        if shareeView is None:
+            FailedCrossPodRequestError("Invalid shared resource specified")
+
         resourceID = message.get("resource_id", None)
         if resourceID is not None:
-            objectResource = yield ownerHomeChild.objectResourceWithID(resourceID)
+            objectResource = yield shareeView.objectResourceWithID(resourceID)
             if objectResource is None:
                 FailedCrossPodRequestError("Invalid owner shared object resource specified")
         else:
             objectResource = None
 
-        returnValue((ownerHome, ownerHomeChild, objectResource,))
+        returnValue((shareeView, objectResource,))
 
 
     #
@@ -451,6 +517,8 @@
         @type shareeView: L{CommonHomeChildExternal}
         @param objectResource: the resource being operated on, or C{None} for classmethod.
         @type objectResource: L{CommonObjectResourceExternal}
+        @param transform: a function used to convert the JSON result into return values.
+        @type transform: C{callable}
         @param args: list of optional arguments.
         @type args: C{list}
         @param kwargs: optional keyword arguments.
@@ -485,41 +553,112 @@
         @type transform: C{callable}
         """
 
-        _ignore_ownerHome, ownerHomeChild, objectResource = yield self._recv(txn, message, actionName)
+        shareeView, 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", {}))
+                value = yield getattr(shareeView, 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", {}))
+                    value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *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,
+            "value": transform(value, shareeView, objectResource) if transform is not None else value,
         })
 
 
+    @inlineCallbacks
+    def send_freebusy(
+        self,
+        calresource,
+        timerange,
+        matchtotal,
+        excludeuid,
+        organizer,
+        organizerPrincipal,
+        same_calendar_user,
+        servertoserver,
+        event_details,
+    ):
+        action, recipient = self._send("freebusy", calresource)
+        action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
+        action["matchtotal"] = matchtotal
+        action["excludeuid"] = excludeuid
+        action["organizer"] = organizer
+        action["organizerPrincipal"] = organizerPrincipal
+        action["same_calendar_user"] = same_calendar_user
+        action["servertoserver"] = servertoserver
+        action["event_details"] = event_details
+        result = yield self.sendRequest(calresource._txn, recipient, action)
+        if result["result"] == "ok":
+            returnValue((result["fbresults"], result["matchtotal"],))
+        elif result["result"] == "exception":
+            raise namedClass(result["class"])(result["message"])
+
+
+    @inlineCallbacks
+    def recv_freebusy(self, txn, message):
+        """
+        Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
+        try:
+            # Operate on the L{CommonHomeChild}
+            fbinfo = [[], [], []]
+            matchtotal = yield generateFreeBusyInfo(
+                shareeView,
+                fbinfo,
+                TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
+                message["matchtotal"],
+                message["excludeuid"],
+                message["organizer"],
+                message["organizerPrincipal"],
+                message["same_calendar_user"],
+                message["servertoserver"],
+                message["event_details"],
+                logItems=None
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "message": str(e),
+            })
+
+        for i in range(3):
+            for j in range(len(fbinfo[i])):
+                fbinfo[i][j] = fbinfo[i][j].getText()
+
+        returnValue({
+            "result": "ok",
+            "fbresults": fbinfo,
+            "matchtotal": matchtotal,
+        })
+
+
     @staticmethod
-    def _transform_string(value, ownerHomeChild, objectResource):
+    def _result_string(value, shareeView, objectResource):
         return str(value)
 
 
     @staticmethod
-    def _transform_externalize(value, ownerHomeChild, objectResource):
-        if isinstance(value, ownerHomeChild._objectResourceClass):
+    def _result_externalize(value, shareeView, objectResource):
+        if isinstance(value, shareeView._objectResourceClass):
             value = value.externalize()
         elif value is not None:
             value = [v.externalize() for v in value]
@@ -528,14 +667,34 @@
 
     @classmethod
     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))
+        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)
+        )
 
 
     @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))
+        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
@@ -547,10 +706,10 @@
 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("loadallobjects", "loadAllObjects", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("create", "create", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("setcomponent", "setComponent")
+PoddingConduit._make_simple_object_action("component", "component", transform_result=PoddingConduit._result_string)
 PoddingConduit._make_simple_object_action("remove", "remove")

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -139,6 +139,11 @@
             self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
 
+        # Log extended item
+        if not hasattr(request, "extendedLogItems"):
+            request.extendedLogItems = {}
+        request.extendedLogItems["xpod"] = j["action"] if "action" in j else "unknown"
+
         # Get the conduit to process the data
         try:
             result = yield self.store.conduit.processRequest(j)

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-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -32,6 +32,9 @@
 from twistedcaldav.ical import Component, normalize_iCalStr
 from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
     ObjectResourceNameNotAllowedError
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from twistedcaldav.caldavxml import TimeRange
+from pycalendar.period import Period
 
 class TestConduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
 
@@ -770,6 +773,7 @@
         self.assertTrue(resource is not None)
         self.assertEqual(resource.name(), "1.ics")
         self.assertEqual(resource.uid(), "uid1")
+        self.assertFalse(resource._componentChanged)
         yield self.otherCommit()
 
         shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
@@ -884,3 +888,42 @@
         object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
         self.assertTrue(object1 is None)
         yield self.commit()
+
+
+    @inlineCallbacks
+    def test_freebusy(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()
+
+        fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
+        fbend = "{now:04d}0103T000000Z".format(**self.nowYear)
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+
+        fbinfo = [[], [], []]
+        matchtotal = yield generateFreeBusyInfo(
+            shared,
+            fbinfo,
+            TimeRange(start=fbstart, end=fbend),
+            0,
+            excludeuid=None,
+            organizer=None,
+            organizerPrincipal=None,
+            same_calendar_user=False,
+            servertoserver=False,
+            event_details=False,
+            logItems=None
+        )
+
+        self.assertEqual(matchtotal, 1)
+        self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
+        self.assertEqual(len(fbinfo[1]), 0)
+        self.assertEqual(len(fbinfo[2]), 0)
+        yield self.otherCommit()

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py	2013-12-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -26,6 +26,8 @@
 
 import twext.web2.dav.test.util
 from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
+from twext.enterprise.ienterprise import AlreadyFinishedError
+import json
 
 class FakeConduitRequest(object):
     """
@@ -52,7 +54,7 @@
     def __init__(self, server, data):
 
         self.server = server
-        self.data = data
+        self.data = json.dumps(data)
 
 
     @inlineCallbacks
@@ -61,6 +63,7 @@
         # Generate an HTTP client request
         try:
             response = (yield self._processRequest())
+            response = json.loads(response)
         except Exception as e:
             raise ValueError("Failed cross-pod request: {}".format(e))
 
@@ -77,7 +80,8 @@
         """
 
         store = self.storeMap[self.server.details()]
-        result = yield store.conduit.processRequest(self.data)
+        result = yield store.conduit.processRequest(json.loads(self.data))
+        result = json.dumps(result)
         returnValue(result)
 
 
@@ -121,10 +125,24 @@
     def newOtherTransaction(self):
         assert self.otherTransaction is None
         store2 = self.otherStoreUnderTest()
-        self.otherTransaction = store2.newTransaction()
+        txn = store2.newTransaction()
+        @inlineCallbacks
+        def maybeCommitThis():
+            try:
+                yield txn.commit()
+            except AlreadyFinishedError:
+                pass
+        self.addCleanup(maybeCommitThis)
+        self.otherTransaction = txn
         return self.otherTransaction
 
 
+    def otherTransactionUnderTest(self):
+        if self.otherTransaction is None:
+            self.newOtherTransaction()
+        return self.otherTransaction
+
+
     @inlineCallbacks
     def otherCommit(self):
         assert self.otherTransaction is not None
@@ -190,13 +208,24 @@
 
 
     @inlineCallbacks
-    def createShare(self, ownerGUID, shareeGUID, name="calendar"):
+    def createShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
 
         home = yield self.homeUnderTest(name=ownerGUID, create=True)
-        calendar = yield home.calendarWithName("calendar")
+        calendar = yield home.calendarWithName(name)
         yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
         yield self.commit()
 
         home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
         yield home2.acceptShare("shared-calendar")
         yield self.otherCommit()
+
+        returnValue("shared-calendar")
+
+
+    @inlineCallbacks
+    def removeShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
+
+        home = yield self.homeUnderTest(name=ownerGUID)
+        calendar = yield home.calendarWithName(name)
+        yield calendar.uninviteUserFromShare(shareeGUID)
+        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-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -85,6 +85,7 @@
 from zope.interface import implements, directlyProvides
 
 from collections import namedtuple
+import itertools
 import json
 import sys
 import time
@@ -1471,7 +1472,7 @@
         if shareeView is not None:
             yield shareeView.declineShare()
 
-        returnValue(shareeView)
+        returnValue(shareeView is not None)
 
 
     #
@@ -1491,12 +1492,30 @@
         # Try to find owner calendar via its external id
         ownerView = yield ownerHome.childWithExternalID(ownerRID)
         if ownerView is None:
-            ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+            try:
+                ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+            except HomeChildNameAlreadyExistsError:
+                # This is odd - it means we possibly have a left over sharer collection which the sharer likely removed
+                # and re-created with the same name but now it has a different externalID and is not found by the initial
+                # query. What we do is check to see whether any shares still reference the old ID - if they do we are hosed.
+                # If not, we can remove the old item and create a new one.
+                oldOwnerView = yield ownerHome.childWithName(ownerName)
+                invites = yield oldOwnerView.sharingInvites()
+                if len(invites) != 0:
+                    log.error("External invite collection name is present with a different externalID and still has shares")
+                    raise
+                log.error("External invite collection name is present with a different externalID - trying to fix")
+                yield ownerHome.removeExternalChild(oldOwnerView)
+                ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+
             if supported_components is not None and hasattr(ownerView, "setSupportedComponents"):
                 yield ownerView.setSupportedComponents(supported_components)
 
         # Now carry out the share operation
-        yield ownerView.inviteUserToShare(self.uid(), bindMode, summary, shareName=shareUID)
+        if bindMode == _BIND_MODE_DIRECT:
+            yield ownerView.directShareWithUser(self.uid(), shareName=shareUID)
+        else:
+            yield ownerView.inviteUserToShare(self.uid(), bindMode, summary, shareName=shareUID)
 
 
     @inlineCallbacks
@@ -1518,7 +1537,12 @@
         # Now carry out the share operation
         yield ownerView.uninviteUserFromShare(self.uid())
 
+        # See if there are any references to the external share - if not remove it
+        invites = yield ownerView.sharingInvites()
+        if len(invites) == 0:
+            yield ownerHome.removeExternalChild(ownerView)
 
+
     @inlineCallbacks
     def processExternalReply(self, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
         """
@@ -1544,7 +1568,29 @@
             yield shareeHome.declineShare(shareUID)
 
 
+    @inlineCallbacks
+    def processExternalRemove(self, ownerUID, shareeUID, shareUID):
+        """
+        External invite received.
+        """
 
+        # Make sure the shareeUID and shareUID match
+
+        # Get the owner home - create external one if not present
+        shareeHome = yield self._txn.homeWithUID(self._homeType, shareeUID)
+        if shareeHome is None or not shareeHome.external():
+            raise ExternalShareFailed("Invalid sharee UID: {}".format(shareeUID))
+
+        # Try to find owner calendar via its external id
+        shareeView = yield shareeHome.anyObjectWithShareUID(shareUID)
+        if shareeView is None:
+            raise ExternalShareFailed("Invalid share UID: {}".format(shareUID))
+
+        # Now carry out the share operation
+        yield shareeView.deleteShare()
+
+
+
 class CommonHome(SharingHomeMixIn):
     log = Logger()
 
@@ -2158,6 +2204,10 @@
         record a revision for the sharee home and sharee collection name with the "deleted" flag set. That way
         the shared collection can be reported as removed.
 
+        For external shared collections we need to report them as invalid as we cannot aggregate the sync token
+        for this home with the sync token from the external share which is under the control of the other pod.
+        Reporting it as invalid means that clients should do requests directly on the share itself to sync it.
+
         @param revision: the sync revision to compare to
         @type revision: C{str}
         @param depth: depth for determine what changed
@@ -2176,6 +2226,7 @@
 
         changed = set()
         deleted = set()
+        invalid = set()
         deleted_collections = set()
         changed_collections = set()
         for path, name, wasdeleted in results:
@@ -2211,40 +2262,52 @@
         shares = yield self.children()
         for share in shares:
             if not share.owned():
-                sharerevision = 0 if revision < share._bindRevision else revision
-                results = [
-                    (
-                        share.name(),
-                        name if name else "",
-                        wasdeleted
-                    )
-                    for name, wasdeleted in
-                    (yield Select([rev.RESOURCE_NAME, rev.DELETED],
-                                     From=rev,
-                                    Where=(rev.REVISION > sharerevision).And(
-                                    rev.RESOURCE_ID == share._resourceID)).on(self._txn))
-                    if name
-                ]
+                if share.external():
+                    if depth == "1":
+                        pass
+                    else:
+                        name = share.name() + "/"
+                        invalid.add(name)
+                        if name in changed:
+                            changed.remove(name)
+                        if name in deleted:
+                            deleted.remove(name)
+                else:
+                    sharerevision = 0 if revision < share._bindRevision else revision
+                    results = [
+                        (
+                            share.name(),
+                            name if name else "",
+                            wasdeleted
+                        )
+                        for name, wasdeleted in
+                        (yield Select([rev.RESOURCE_NAME, rev.DELETED],
+                                         From=rev,
+                                        Where=(rev.REVISION > sharerevision).And(
+                                        rev.RESOURCE_ID == share._resourceID)).on(self._txn))
+                        if name
+                    ]
 
-                for path, name, wasdeleted in results:
-                    if wasdeleted:
-                        if sharerevision:
-                            if depth == "1":
-                                changed.add("%s/" % (path,))
-                            else:
-                                deleted.add("%s/%s" % (path, name,))
+                    for path, name, wasdeleted in results:
+                        if wasdeleted:
+                            if sharerevision:
+                                if depth == "1":
+                                    changed.add("%s/" % (path,))
+                                else:
+                                    deleted.add("%s/%s" % (path, name,))
 
-                for path, name, wasdeleted in results:
-                    # Always report collection as changed
-                    changed.add("%s/" % (path,))
-                    if name:
-                        # Resource changed - for depth "infinity" report resource as changed
-                        if depth != "1":
-                            changed.add("%s/%s" % (path, name,))
+                    for path, name, wasdeleted in results:
+                        # Always report collection as changed
+                        changed.add("%s/" % (path,))
+                        if name:
+                            # Resource changed - for depth "infinity" report resource as changed
+                            if depth != "1":
+                                changed.add("%s/%s" % (path, name,))
 
         changed = sorted(changed)
         deleted = sorted(deleted)
-        returnValue((changed, deleted))
+        invalid = sorted(invalid)
+        returnValue((changed, deleted, invalid,))
 
 
     @inlineCallbacks
@@ -3084,6 +3147,11 @@
         if shareeView is None:
             shareeView = yield self.createShare(shareeUID=shareeUID, mode=_BIND_MODE_DIRECT, shareName=shareName)
             yield shareeView.newShare()
+
+            # Check for external
+            if shareeView.viewerHome().external():
+                yield self._sendExternalInvite(shareeView)
+
         returnValue(shareeView)
 
 
@@ -3154,6 +3222,8 @@
         ownerView = yield self.ownerView()
         if self.direct():
             yield ownerView.removeShare(self)
+            if not ownerView.external():
+                yield self._removeExternalInvite(ownerView)
         else:
             yield self.declineShare()
 
@@ -3303,6 +3373,18 @@
         )
 
 
+    @inlineCallbacks
+    def _removeExternalInvite(self):
+
+        yield self._txn.store().conduit.send_shareremove(
+            self._txn,
+            self.viewerHome()._homeType,
+            self.ownerHome().uid(),
+            self.viewerHome().uid(),
+            self.shareName(),
+        )
+
+
     #
     # Lower level API
     #
@@ -4695,7 +4777,7 @@
         @type revision: C{int}
         """
 
-        if revision < self._bindRevision:
+        if revision < self._bindRevision and not self.external():
             revision = 0
         return super(CommonHomeChild, self).resourceNamesSinceRevision(revision)
 
@@ -5225,17 +5307,25 @@
         )
 
 
+    @classmethod
+    def _otherSerializedAttributes(cls): #@NoSelf
+        return (
+            "_componentChanged",
+        )
+
+
     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()])
+        return dict([(attr[1:], getattr(self, attr, None)) for attr in itertools.chain(self._rowAttributes(), self._otherSerializedAttributes())])
 
 
     @classmethod
-    def internalize(cls, mapping):
+    @inlineCallbacks
+    def internalize(cls, parent, 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}.
@@ -5243,7 +5333,10 @@
         C{None} and ignore extra items.
         """
 
-        return [mapping.get(row[1:]) for row in cls._rowAttributes()]
+        child = yield cls.makeClass(parent, [mapping.get(row[1:]) for row in cls._rowAttributes()])
+        for attr in cls._otherSerializedAttributes():
+            setattr(child, attr, mapping.get(attr[1:]))
+        returnValue(child)
 
 
     @inlineCallbacks
@@ -5356,18 +5449,10 @@
         self._locked = True
 
 
-    def setComponent(self, component, inserting=False, options=None):
+    def setComponent(self, component, inserting=False):
         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
 

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-14 06:28:16 UTC (rev 12110)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py	2013-12-16 17:12:05 UTC (rev 12111)
@@ -14,21 +14,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from txdav.common.icommondatastore import NonExistentExternalShare, \
-    ExternalShareFailed
 """
 SQL data store.
 """
 
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-
 from twext.internet.decorate import memoizedKey
 from twext.python.log import Logger
 
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
 from txdav.base.propertystore.sql import PropertyStore
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource
 from txdav.common.datastore.sql_tables import _HOME_STATUS_EXTERNAL
+from txdav.common.icommondatastore import NonExistentExternalShare, \
+    ExternalShareFailed
 
 
 log = Logger()
@@ -116,6 +116,17 @@
         raise AssertionError("CommonHomeExternal: not supported")
 
 
+    @inlineCallbacks
+    def removeExternalChild(self, child):
+        """
+        Remove an external child. Check that it is invalid or unused before calling this because if there
+        are valid references to it, removing will break things.
+        """
+        if child._externalID is None:
+            raise AssertionError("CommonHomeExternal: not supported")
+        yield super(CommonHomeExternal, self).removeChildWithName(child.name())
+
+
     def syncToken(self):
         """
         No children.
@@ -217,11 +228,16 @@
         yield ownerView.removeShare(self)
 
 
-    def remove(self, rid):
+    @inlineCallbacks
+    def remove(self):
         """
-        External shares are never removed directly - instead they must be "uninvited".
+        External shares are never removed directly - instead they must be "uninvited". However,
+        the owner's external calendar can be removed.
         """
-        raise AssertionError("CommonHomeChildExternal: not supported")
+        if self.owned():
+            yield super(CommonHomeChildExternal, self).remove()
+        else:
+            raise AssertionError("CommonHomeChildExternal: not supported")
 
 
     @inlineCallbacks
@@ -291,14 +307,6 @@
 
 
     @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
@@ -348,7 +356,7 @@
         results = []
         if mapping_list:
             for mapping in mapping_list:
-                child = yield cls.makeClass(parent, cls.internalize(mapping))
+                child = yield cls.internalize(parent, mapping)
                 results.append(child)
         returnValue(results)
 
@@ -361,7 +369,7 @@
         results = []
         if mapping_list:
             for mapping in mapping_list:
-                child = yield cls.makeClass(parent, cls.internalize(mapping))
+                child = yield cls.internalize(parent, mapping)
                 results.append(child)
         returnValue(results)
 
@@ -372,7 +380,7 @@
         mapping = yield parent._txn.store().conduit.send_objectwith(parent, None, name, uid, resourceID)
 
         if mapping:
-            child = yield cls.makeClass(parent, cls.internalize(mapping))
+            child = yield cls.internalize(parent, mapping)
             returnValue(child)
         else:
             returnValue(None)
@@ -381,20 +389,20 @@
     @classmethod
     @inlineCallbacks
     def create(cls, parent, name, component, options=None):
-        mapping = yield parent._txn.store().conduit.send_create(parent, None, name, component, options=options)
+        mapping = yield parent._txn.store().conduit.send_create(parent, None, name, str(component), options=options)
 
         if mapping:
-            child = yield cls.makeClass(parent, cls.internalize(mapping))
+            child = yield cls.internalize(parent, 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)
+    def setComponent(self, component, **kwargs):
+        self._componentChanged = yield self._txn.store().conduit.send_setcomponent(self.parentCollection(), self, str(component), **kwargs)
         self._cachedComponent = None
-        returnValue(changed)
+        returnValue(self._componentChanged)
 
 
     @inlineCallbacks
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/0021a37c/attachment.html>


More information about the calendarserver-changes mailing list