<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[12111] CalendarServer/branches/users/cdaboo/cross-pod-sharing</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/12111">12111</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2013-12-16 09:12:05 -0800 (Mon, 16 Dec 2013)</dd>
</dl>

<h3>Log Message</h3>
<pre>Checkpoint: fixes for freebusy and sync, plus other bits. Working through complete set of cross-pod CDT items. More to come.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtwistedcaldavresourcepy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/resource.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtwistedcaldavsharingpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/sharing.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtwistedcaldavstorebridgepy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/storebridge.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoreschedulingfreebusypy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/freebusy.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoreschedulingischedulexmlpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/ischedule/xml.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoresqlpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoretestcommonpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoretesttest_sql_sharingpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_sharing.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcarddavdatastoresqlpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcarddavdatastoretesttest_sql_sharingpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql_sharing.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingconduitpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingresourcepy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingtesttest_conduitpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingtestutilpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastoresqlpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py</a></li>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastoresql_externalpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoretesttest_sql_externalpy">CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtwistedcaldavresourcepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/resource.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -2667,10 +2667,9 @@
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def _indexWhatChanged(self, revision, depth):
</span><span class="cx">         # The newstore implementation supports this directly
</span><del>-        changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
</del><ins>+        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
</ins><span class="cx">             revision, depth
</span><span class="cx">         )
</span><del>-        notallowed = []
</del><span class="cx"> 
</span><span class="cx">         # Need to insert some addition items on first sync
</span><span class="cx">         if revision == 0:
</span><span class="lines">@@ -2894,10 +2893,9 @@
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def _indexWhatChanged(self, revision, depth):
</span><span class="cx">         # The newstore implementation supports this directly
</span><del>-        changed, deleted = yield self._newStoreHome.resourceNamesSinceToken(
</del><ins>+        changed, deleted, notallowed = yield self._newStoreHome.resourceNamesSinceToken(
</ins><span class="cx">             revision, depth
</span><span class="cx">         )
</span><del>-        notallowed = []
</del><span class="cx"> 
</span><span class="cx">         # Need to insert some addition items on first sync
</span><span class="cx">         if revision == 0:
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtwistedcaldavsharingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/sharing.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -805,6 +805,12 @@
</span><span class="cx"> 
</span><span class="cx">         # Accept the share
</span><span class="cx">         shareeView = yield self._newStoreHome.acceptShare(inviteUID, summary)
</span><ins>+        if shareeView is None:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, &quot;invalid-share&quot;),
+                &quot;Invite UID not valid&quot;,
+            ))
</ins><span class="cx"> 
</span><span class="cx">         # Return the URL of the shared collection
</span><span class="cx">         sharedAsURL = joinURL(self.url(), shareeView.shareName())
</span><span class="lines">@@ -820,7 +826,13 @@
</span><span class="cx">     def declineShare(self, request, inviteUID):
</span><span class="cx"> 
</span><span class="cx">         # Remove it if it is in the DB
</span><del>-        yield self._newStoreHome.declineShare(inviteUID)
</del><ins>+        result = yield self._newStoreHome.declineShare(inviteUID)
+        if not result:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (calendarserver_namespace, &quot;invalid-share&quot;),
+                &quot;Invite UID not valid&quot;,
+            ))
</ins><span class="cx">         returnValue(Response(code=responsecode.NO_CONTENT))
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtwistedcaldavstorebridgepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/twistedcaldav/storebridge.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -462,6 +462,8 @@
</span><span class="cx">         if self.isShareeResource():
</span><span class="cx">             log.debug(&quot;Removing shared collection %s&quot; % (self,))
</span><span class="cx">             yield self.removeShareeResource(request)
</span><ins>+            # Re-initialize to get stuff setup again now we have no object
+            self._initializeWithHomeChild(None, self._parentResource)
</ins><span class="cx">             returnValue(NO_CONTENT)
</span><span class="cx"> 
</span><span class="cx">         log.debug(&quot;Deleting collection %s&quot; % (self,))
</span><span class="lines">@@ -3371,6 +3373,8 @@
</span><span class="cx">         if self.isShareeResource():
</span><span class="cx">             log.debug(&quot;Removing shared resource %s&quot; % (self,))
</span><span class="cx">             yield self.removeShareeResource(request)
</span><ins>+            # Re-initialize to get stuff setup again now we have no object
+            self._initializeWithObject(None, self._newStoreParent)
</ins><span class="cx">             returnValue(NO_CONTENT)
</span><span class="cx">         elif self._newStoreObject.isGroupForSharedAddressBook():
</span><span class="cx">             abCollectionResource = (yield request.locateResource(parentForURL(request.uri)))
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoreschedulingfreebusypy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/freebusy.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -92,7 +92,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-@inlineCallbacks
</del><span class="cx"> def generateFreeBusyInfo(
</span><span class="cx">     calresource,
</span><span class="cx">     fbinfo,
</span><span class="lines">@@ -107,6 +106,86 @@
</span><span class="cx">     logItems=None,
</span><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><ins>+    Get freebusy information for a calendar. Different behavior for internal vs external calendars.
+
+    See L{_internalGenerateFreeBusyInfo} for argument description.
+    &quot;&quot;&quot;
+
+    # 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
+        )
+
+
+
+@inlineCallbacks
+def _externalGenerateFreeBusyInfo(
+    calresource,
+    fbinfo,
+    timerange,
+    matchtotal,
+    excludeuid=None,
+    organizer=None,
+    organizerPrincipal=None,
+    same_calendar_user=False,
+    servertoserver=False,
+    event_details=None,
+    logItems=None,
+):
+    &quot;&quot;&quot;
+    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.
+    &quot;&quot;&quot;
+    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)
+
+
+
+@inlineCallbacks
+def _internalGenerateFreeBusyInfo(
+    calresource,
+    fbinfo,
+    timerange,
+    matchtotal,
+    excludeuid=None,
+    organizer=None,
+    organizerPrincipal=None,
+    same_calendar_user=False,
+    servertoserver=False,
+    event_details=None,
+    logItems=None,
+):
+    &quot;&quot;&quot;
</ins><span class="cx">     Run a free busy report on the specified calendar collection
</span><span class="cx">     accumulating the free busy info for later processing.
</span><span class="cx">     @param calresource: the L{Calendar} for a calendar collection.
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoreschedulingischedulexmlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/scheduling/ischedule/xml.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -259,14 +259,18 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><del>-    def fromCalendar(clazz, calendar):
</del><ins>+    def fromCalendar(clazz, calendar, format=None):
+        attrs = {}
+        if format is not None and format != &quot;text/calendar&quot;:
+            attrs[&quot;content-type&quot;] = format
+
</ins><span class="cx">         if isinstance(calendar, str):
</span><span class="cx">             if not calendar:
</span><span class="cx">                 raise ValueError(&quot;Missing calendar data&quot;)
</span><span class="cx">             return clazz(PCDATAElement(calendar))
</span><span class="cx">         elif isinstance(calendar, iComponent):
</span><span class="cx">             assert calendar.name() == &quot;VCALENDAR&quot;, &quot;Not a calendar: %r&quot; % (calendar,)
</span><del>-            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
</del><ins>+            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=format)))
</ins><span class="cx">         else:
</span><span class="cx">             raise ValueError(&quot;Not a calendar: %s&quot; % (calendar,))
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -2147,6 +2147,8 @@
</span><span class="cx">         Scheduling will be done automatically.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><ins>+        if isinstance(component, str) or isinstance(component, unicode):
+            component = self._componentClass.fromString(component)
</ins><span class="cx">         return self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL, smart_merge)
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoretestcommonpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -1602,7 +1602,7 @@
</span><span class="cx"> 
</span><span class="cx">         home = yield self.homeUnderTest()
</span><span class="cx"> 
</span><del>-        changed, deleted = yield home.resourceNamesSinceToken(
</del><ins>+        changed, deleted, invalid = yield home.resourceNamesSinceToken(
</ins><span class="cx">             self.token2revision(st), &quot;infinity&quot;)
</span><span class="cx"> 
</span><span class="cx">         self.assertEquals(set(changed), set([&quot;calendar_1/&quot;,
</span><span class="lines">@@ -1610,11 +1610,13 @@
</span><span class="cx">                                              &quot;calendar_1/2.ics&quot;,
</span><span class="cx">                                              &quot;other-calendar/&quot;]))
</span><span class="cx">         self.assertEquals(set(deleted), set([&quot;calendar_1/2.ics&quot;]))
</span><ins>+        self.assertEquals(invalid, [])
</ins><span class="cx"> 
</span><del>-        changed, deleted = yield home.resourceNamesSinceToken(
</del><ins>+        changed, deleted, invalid = yield home.resourceNamesSinceToken(
</ins><span class="cx">             self.token2revision(st2), &quot;infinity&quot;)
</span><span class="cx">         self.assertEquals(changed, [])
</span><span class="cx">         self.assertEquals(deleted, [])
</span><ins>+        self.assertEquals(invalid, [])
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoretesttest_sql_externalpy"></a>
<div class="addfile"><h4>Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_external.py (0 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -0,0 +1,680 @@
</span><ins>+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; 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):
+
+    &quot;&quot;&quot;
+    Test store-based calendar sharing.
+    &quot;&quot;&quot;
+
+    @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 = &quot;&quot;&quot;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
+&quot;&quot;&quot;
+
+    @classproperty(cache=False)
+    def requirements(cls): #@NoSelf
+        return {
+        &quot;user01&quot;: {
+            &quot;calendar&quot;: {
+                &quot;cal1.ics&quot;: (cls.cal1, None,),
+            },
+            &quot;inbox&quot;: {
+            },
+        },
+        &quot;user02&quot;: {
+            &quot;calendar&quot;: {
+            },
+            &quot;inbox&quot;: {
+            },
+        },
+        &quot;user03&quot;: {
+            &quot;calendar&quot;: {
+            },
+            &quot;inbox&quot;: {
+            },
+        },
+    }
+
+
+
+class CalendarSharing(BaseSharingTests):
+
+    @inlineCallbacks
+    def test_no_shares(self):
+        &quot;&quot;&quot;
+        Test that initially there are no shares.
+        &quot;&quot;&quot;
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_invite_sharee(self):
+        &quot;&quot;&quot;
+        Test invite/uninvite creates/removes shares and notifications.
+        &quot;&quot;&quot;
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare(&quot;puser02&quot;, _BIND_MODE_READ, &quot;summary&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertEqual(invites[0].uid, shareeView.shareUID())
+        self.assertEqual(invites[0].ownerUID, &quot;user01&quot;)
+        self.assertEqual(invites[0].shareeUID, &quot;puser02&quot;)
+        self.assertEqual(invites[0].mode, _BIND_MODE_READ)
+        self.assertEqual(invites[0].status, _BIND_STATUS_INVITED)
+        self.assertEqual(invites[0].summary, &quot;summary&quot;)
+
+        inviteUID = shareeView.shareUID()
+        sharedName = shareeView.name()
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID(&quot;puser02&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID, ])
+        yield self.otherCommit()
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+
+        yield calendar.uninviteUserFromShare(&quot;puser02&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID(&quot;puser02&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [])
+        yield self.otherCommit()
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertTrue(calendar.isShared())
+        yield calendar.setShared(False)
+        self.assertFalse(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_share(self):
+        &quot;&quot;&quot;
+        Test that invite+accept creates shares and notifications.
+        &quot;&quot;&quot;
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare(&quot;puser02&quot;, _BIND_MODE_READ, &quot;summary&quot;)
+        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=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID(&quot;puser02&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Re-accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_decline_share(self):
+        &quot;&quot;&quot;
+        Test that invite+decline does not create shares but does create notifications.
+        &quot;&quot;&quot;
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare(&quot;puser02&quot;, _BIND_MODE_READ, &quot;summary&quot;)
+        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=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID(&quot;puser02&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Decline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Redecline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_decline_share(self):
+        &quot;&quot;&quot;
+        Test that invite+accept/decline creates/removes shares and notifications.
+        Decline via the home.
+        &quot;&quot;&quot;
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.inviteUserToShare(&quot;puser02&quot;, _BIND_MODE_READ, &quot;summary&quot;)
+        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=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID(&quot;puser02&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertTrue(calendar.isShared())
+
+        yield self.commit()
+
+        # Decline
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        yield shareeHome.declineShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertTrue(calendar.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_remove_share(self):
+        &quot;&quot;&quot;
+        Test that invite+accept/decline creates/removes shares and notifications.
+        Decline via the shared collection (removal).
+        &quot;&quot;&quot;
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        shareeView = yield calendar.inviteUserToShare(&quot;puser02&quot;, _BIND_MODE_READ, &quot;summary&quot;)
+        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=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+
+        notifyHome = yield txn2.notificationsWithUID(&quot;puser02&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 1)
+        yield self.otherCommit()
+
+        # Accept
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        yield shareeHome.acceptShare(inviteUID)
+
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is not None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+        yield self.commit()
+
+        # Delete
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        yield shared.deleteShare()
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(notifications, [inviteUID + &quot;-reply&quot;, ])
+
+
+    @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=&quot;puser02&quot;, name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_accept_remove_accept_newcalendar(self):
+        &quot;&quot;&quot;
+        Test that deleting and re-creating a share with the same sharer name works.
+        &quot;&quot;&quot;
+
+        home = yield self.homeUnderTest(name=&quot;user01&quot;, create=True)
+        yield home.createCalendarWithName(&quot;shared&quot;)
+        yield self.commit()
+
+        shared_name = yield self.createShare(name=&quot;shared&quot;)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+        yield self.removeShare(name=&quot;shared&quot;)
+        home = yield self.homeUnderTest(name=&quot;user01&quot;, create=True)
+        yield home.removeCalendarWithName(&quot;shared&quot;)
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=shared_name)
+        self.assertTrue(otherCal is None)
+        yield self.otherCommit()
+
+        home = yield self.homeUnderTest(name=&quot;user01&quot;, create=True)
+        yield home.createCalendarWithName(&quot;shared&quot;)
+        yield self.commit()
+
+        shared_name = yield self.createShare(name=&quot;shared&quot;)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=shared_name)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_inviteProperties(self):
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        yield calendar.setUsedForFreeBusy(True)
+        yield self.commit()
+
+        shared_name = yield self.createShare()
+
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=shared_name)
+        self.assertFalse(shared.isUsedForFreeBusy())
+
+
+    @inlineCallbacks
+    def test_direct_sharee(self):
+        &quot;&quot;&quot;
+        Test invite/uninvite creates/removes shares and notifications.
+        &quot;&quot;&quot;
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        shareeView = yield calendar.directShareWithUser(&quot;puser02&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertEqual(invites[0].uid, shareeView.shareUID())
+        self.assertEqual(invites[0].ownerUID, &quot;user01&quot;)
+        self.assertEqual(invites[0].shareeUID, &quot;puser02&quot;)
+        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=&quot;user02&quot;, name=sharedName)
+        self.assertTrue(shared is not None)
+
+        notifyHome = yield txn2.notificationsWithUID(&quot;user02&quot;)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(len(notifications), 0)
+        yield self.otherCommit()
+
+        # Remove
+        txn2 = self.newOtherTransaction()
+        shared = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        yield shared.deleteShare()
+        yield self.otherCommit()
+
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(&quot;user01&quot;)
+        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=&quot;user01&quot;)
+        self.assertEquals(home.notifierID(), (&quot;CalDAV&quot;, &quot;user01&quot;,))
+        calendar = yield home.calendarWithName(&quot;calendar&quot;)
+        self.assertEquals(calendar.notifierID(), (&quot;CalDAV&quot;, &quot;user01/calendar&quot;,))
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        home = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        self.assertEquals(home.notifierID(), (&quot;CalDAV&quot;, &quot;puser02&quot;,))
+        calendar = yield home.calendarWithName(shared_name)
+        self.assertEquals(calendar.notifierID(), (&quot;CalDAV&quot;, &quot;user01/calendar&quot;,))
+
+
+    @inlineCallbacks
+    def test_sharedWithTwo(self):
+        shared_name1 = yield self.createShare(shareeGUID=&quot;puser02&quot;)
+        shared_name2 = yield self.createShare(shareeGUID=&quot;puser03&quot;)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=shared_name1)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser03&quot;, name=shared_name2)
+        self.assertTrue(otherCal is not None)
+        yield self.otherCommit()
+
+
+
+class SharingRevisions(BaseSharingTests):
+    &quot;&quot;&quot;
+    Test store-based sharing and interaction with revision table.
+    &quot;&quot;&quot;
+
+    @inlineCallbacks
+    def test_shareWithRevision(self):
+        &quot;&quot;&quot;
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        &quot;&quot;&quot;
+        sharedName = yield self.createShare()
+
+        normalCal = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_updateShareRevision(self):
+        &quot;&quot;&quot;
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        &quot;&quot;&quot;
+        # Invite
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        shareeView = yield calendar.inviteUserToShare(&quot;puser02&quot;, _BIND_MODE_READ, &quot;summary&quot;)
+        newCalName = shareeView.shareUID()
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertEqual(normalCal._bindRevision, 0)
+        yield self.commit()
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        otherCal = yield otherHome.anyObjectWithShareUID(newCalName)
+        self.assertEqual(otherCal._bindRevision, 0)
+        yield self.otherCommit()
+
+        txn2 = self.newOtherTransaction()
+        shareeHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        shareeView = yield shareeHome.acceptShare(newCalName)
+        sharedName = shareeView.name()
+        yield self.otherCommit()
+
+        normalCal = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_sharedRevisions(self):
+        &quot;&quot;&quot;
+        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+        &quot;&quot;&quot;
+        sharedName = yield self.createShare()
+
+        normalCal = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        self.assertEqual(normalCal._bindRevision, 0)
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+        otherCal = yield self.calendarUnderTest(txn=txn2, home=&quot;puser02&quot;, 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 (&quot;1&quot;, &quot;infinity&quot;,):
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision - 1, depth)
+            self.assertEqual(len(changed), 0 if depth == &quot;infinity&quot; else 1)
+            self.assertEqual(len(deleted), 0)
+            self.assertEqual(len(invalid), 1 if depth == &quot;infinity&quot; 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 == &quot;infinity&quot; else 0)
+
+        yield self.otherCommit()
+
+        yield self.removeShare()
+
+        txn2 = self.newOtherTransaction()
+        otherHome = yield self.homeUnderTest(txn=txn2, name=&quot;puser02&quot;)
+
+        for depth in (&quot;1&quot;, &quot;infinity&quot;,):
+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 1)
+            self.assertEqual(len(invalid), 0)
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcaldavdatastoretesttest_sql_sharingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql_sharing.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -563,10 +563,12 @@
</span><span class="cx">         self.assertEqual(len(deleted), 0)
</span><span class="cx"> 
</span><span class="cx">         for depth in (&quot;1&quot;, &quot;infinity&quot;,):
</span><del>-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision - 1, depth)
</del><ins>+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision - 1, depth)
</ins><span class="cx">             self.assertNotEqual(len(changed), 0)
</span><span class="cx">             self.assertEqual(len(deleted), 0)
</span><ins>+            self.assertEqual(len(invalid), 0)
</ins><span class="cx"> 
</span><del>-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
</del><ins>+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
</ins><span class="cx">             self.assertEqual(len(changed), 0)
</span><span class="cx">             self.assertEqual(len(deleted), 0)
</span><ins>+            self.assertEqual(len(invalid), 0)
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcarddavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -1956,6 +1956,9 @@
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def setComponent(self, component, inserting=False):
</span><span class="cx"> 
</span><ins>+        if isinstance(component, str) or isinstance(component, unicode):
+            component = self._componentClass.fromString(component)
+
</ins><span class="cx">         self._componentChanged = False
</span><span class="cx"> 
</span><span class="cx">         # Handle all validation operations here.
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcarddavdatastoretesttest_sql_sharingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql_sharing.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -1144,13 +1144,15 @@
</span><span class="cx"> 
</span><span class="cx">         otherHome = yield self.addressbookHomeUnderTest(name=&quot;user02&quot;)
</span><span class="cx">         for depth in (&quot;1&quot;, &quot;infinity&quot;,):
</span><del>-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
</del><ins>+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
</ins><span class="cx">             self.assertNotEqual(len(changed), 0)
</span><span class="cx">             self.assertEqual(len(deleted), 0)
</span><ins>+            self.assertEqual(len(invalid), 0)
</ins><span class="cx"> 
</span><del>-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
</del><ins>+            changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
</ins><span class="cx">             self.assertEqual(len(changed), 0)
</span><span class="cx">             self.assertEqual(len(deleted), 0)
</span><ins>+            self.assertEqual(len(invalid), 0)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingconduitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -22,6 +22,8 @@
</span><span class="cx"> from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
</span><span class="cx"> from txdav.common.icommondatastore import ExternalShareFailed
</span><span class="cx"> from twisted.python.reflect import namedClass
</span><ins>+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from twistedcaldav.caldavxml import TimeRange
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> __all__ = [
</span><span class="lines">@@ -372,6 +374,66 @@
</span><span class="cx">         })
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def send_shareremove(self, txn, homeType, ownerUID, shareeUID, shareUID):
+        &quot;&quot;&quot;
+        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}
+        &quot;&quot;&quot;
+
+        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+        action = {
+            &quot;action&quot;: &quot;shareremove&quot;,
+            &quot;type&quot;: homeType,
+            &quot;owner&quot;: ownerUID,
+            &quot;sharee&quot;: shareeUID,
+            &quot;share_id&quot;: shareUID,
+        }
+
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_shareremove(self, txn, message):
+        &quot;&quot;&quot;
+        Process a sharing remove cross-pod message. Message arguments as per L{send_shareremove}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        &quot;&quot;&quot;
+
+        if message[&quot;action&quot;] != &quot;shareremove&quot;:
+            raise FailedCrossPodRequestError(&quot;Wrong action '{}' for recv_shareremove&quot;.format(message[&quot;action&quot;]))
+
+        # Create a share
+        ownerHome = yield txn.homeWithUID(message[&quot;type&quot;], message[&quot;owner&quot;])
+        if ownerHome is None or ownerHome.external():
+            FailedCrossPodRequestError(&quot;Invalid owner UID specified&quot;)
+
+        try:
+            yield ownerHome.processExternalRemove(
+                message[&quot;owner&quot;],
+                message[&quot;sharee&quot;],
+                message[&quot;share_id&quot;],
+            )
+        except ExternalShareFailed as e:
+            FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            &quot;result&quot;: &quot;ok&quot;,
+        })
+
+
</ins><span class="cx">     #
</span><span class="cx">     # Sharer data access related apis
</span><span class="cx">     #
</span><span class="lines">@@ -415,24 +477,28 @@
</span><span class="cx">         if message[&quot;action&quot;] != expected_action:
</span><span class="cx">             raise FailedCrossPodRequestError(&quot;Wrong action '{}' for recv_{}&quot;.format(message[&quot;action&quot;], expected_action))
</span><span class="cx"> 
</span><del>-        # Create a share
-        ownerHome = yield txn.homeWithUID(message[&quot;type&quot;], message[&quot;owner&quot;], create=True)
</del><ins>+        # Get a share
+        ownerHome = yield txn.homeWithUID(message[&quot;type&quot;], message[&quot;owner&quot;])
</ins><span class="cx">         if ownerHome is None or ownerHome.external():
</span><span class="cx">             FailedCrossPodRequestError(&quot;Invalid owner UID specified&quot;)
</span><span class="cx"> 
</span><del>-        ownerHomeChild = yield ownerHome.childWithID(message[&quot;owner_id&quot;])
-        if ownerHomeChild is None:
-            FailedCrossPodRequestError(&quot;Invalid owner shared resource specified&quot;)
</del><ins>+        shareeHome = yield txn.homeWithUID(message[&quot;type&quot;], message[&quot;sharee&quot;])
+        if shareeHome is None or not shareeHome.external():
+            FailedCrossPodRequestError(&quot;Invalid sharee UID specified&quot;)
</ins><span class="cx"> 
</span><ins>+        shareeView = yield shareeHome.childWithID(message[&quot;owner_id&quot;])
+        if shareeView is None:
+            FailedCrossPodRequestError(&quot;Invalid shared resource specified&quot;)
+
</ins><span class="cx">         resourceID = message.get(&quot;resource_id&quot;, None)
</span><span class="cx">         if resourceID is not None:
</span><del>-            objectResource = yield ownerHomeChild.objectResourceWithID(resourceID)
</del><ins>+            objectResource = yield shareeView.objectResourceWithID(resourceID)
</ins><span class="cx">             if objectResource is None:
</span><span class="cx">                 FailedCrossPodRequestError(&quot;Invalid owner shared object resource specified&quot;)
</span><span class="cx">         else:
</span><span class="cx">             objectResource = None
</span><span class="cx"> 
</span><del>-        returnValue((ownerHome, ownerHomeChild, objectResource,))
</del><ins>+        returnValue((shareeView, objectResource,))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     #
</span><span class="lines">@@ -451,6 +517,8 @@
</span><span class="cx">         @type shareeView: L{CommonHomeChildExternal}
</span><span class="cx">         @param objectResource: the resource being operated on, or C{None} for classmethod.
</span><span class="cx">         @type objectResource: L{CommonObjectResourceExternal}
</span><ins>+        @param transform: a function used to convert the JSON result into return values.
+        @type transform: C{callable}
</ins><span class="cx">         @param args: list of optional arguments.
</span><span class="cx">         @type args: C{list}
</span><span class="cx">         @param kwargs: optional keyword arguments.
</span><span class="lines">@@ -485,41 +553,112 @@
</span><span class="cx">         @type transform: C{callable}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        _ignore_ownerHome, ownerHomeChild, objectResource = yield self._recv(txn, message, actionName)
</del><ins>+        shareeView, objectResource = yield self._recv(txn, message, actionName)
</ins><span class="cx">         try:
</span><span class="cx">             if onHomeChild:
</span><span class="cx">                 # Operate on the L{CommonHomeChild}
</span><del>-                value = yield getattr(ownerHomeChild, method)(*message.get(&quot;arguments&quot;, ()), **message.get(&quot;keywords&quot;, {}))
</del><ins>+                value = yield getattr(shareeView, method)(*message.get(&quot;arguments&quot;, ()), **message.get(&quot;keywords&quot;, {}))
</ins><span class="cx">             else:
</span><span class="cx">                 # Operate on the L{CommonObjectResource}
</span><span class="cx">                 if objectResource is not None:
</span><span class="cx">                     value = yield getattr(objectResource, method)(*message.get(&quot;arguments&quot;, ()), **message.get(&quot;keywords&quot;, {}))
</span><span class="cx">                 else:
</span><span class="cx">                     # classmethod call
</span><del>-                    value = yield getattr(ownerHomeChild._objectResourceClass, method)(ownerHomeChild, *message.get(&quot;arguments&quot;, ()), **message.get(&quot;keywords&quot;, {}))
</del><ins>+                    value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *message.get(&quot;arguments&quot;, ()), **message.get(&quot;keywords&quot;, {}))
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             returnValue({
</span><span class="cx">                 &quot;result&quot;: &quot;exception&quot;,
</span><span class="cx">                 &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
</span><span class="cx">                 &quot;message&quot;: str(e),
</span><span class="cx">             })
</span><del>-        if transform is not None:
-            value = transform(value, ownerHomeChild, objectResource)
</del><span class="cx"> 
</span><span class="cx">         returnValue({
</span><span class="cx">             &quot;result&quot;: &quot;ok&quot;,
</span><del>-            &quot;value&quot;: value,
</del><ins>+            &quot;value&quot;: transform(value, shareeView, objectResource) if transform is not None else value,
</ins><span class="cx">         })
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def send_freebusy(
+        self,
+        calresource,
+        timerange,
+        matchtotal,
+        excludeuid,
+        organizer,
+        organizerPrincipal,
+        same_calendar_user,
+        servertoserver,
+        event_details,
+    ):
+        action, recipient = self._send(&quot;freebusy&quot;, calresource)
+        action[&quot;timerange&quot;] = [timerange.start.getText(), timerange.end.getText()]
+        action[&quot;matchtotal&quot;] = matchtotal
+        action[&quot;excludeuid&quot;] = excludeuid
+        action[&quot;organizer&quot;] = organizer
+        action[&quot;organizerPrincipal&quot;] = organizerPrincipal
+        action[&quot;same_calendar_user&quot;] = same_calendar_user
+        action[&quot;servertoserver&quot;] = servertoserver
+        action[&quot;event_details&quot;] = event_details
+        result = yield self.sendRequest(calresource._txn, recipient, action)
+        if result[&quot;result&quot;] == &quot;ok&quot;:
+            returnValue((result[&quot;fbresults&quot;], result[&quot;matchtotal&quot;],))
+        elif result[&quot;result&quot;] == &quot;exception&quot;:
+            raise namedClass(result[&quot;class&quot;])(result[&quot;message&quot;])
+
+
+    @inlineCallbacks
+    def recv_freebusy(self, txn, message):
+        &quot;&quot;&quot;
+        Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
+
+        @param message: message arguments
+        @type message: C{dict}
+        &quot;&quot;&quot;
+
+        shareeView, _ignore_objectResource = yield self._recv(txn, message, &quot;freebusy&quot;)
+        try:
+            # Operate on the L{CommonHomeChild}
+            fbinfo = [[], [], []]
+            matchtotal = yield generateFreeBusyInfo(
+                shareeView,
+                fbinfo,
+                TimeRange(start=message[&quot;timerange&quot;][0], end=message[&quot;timerange&quot;][1]),
+                message[&quot;matchtotal&quot;],
+                message[&quot;excludeuid&quot;],
+                message[&quot;organizer&quot;],
+                message[&quot;organizerPrincipal&quot;],
+                message[&quot;same_calendar_user&quot;],
+                message[&quot;servertoserver&quot;],
+                message[&quot;event_details&quot;],
+                logItems=None
+            )
+        except Exception as e:
+            returnValue({
+                &quot;result&quot;: &quot;exception&quot;,
+                &quot;class&quot;: &quot;.&quot;.join((e.__class__.__module__, e.__class__.__name__,)),
+                &quot;message&quot;: str(e),
+            })
+
+        for i in range(3):
+            for j in range(len(fbinfo[i])):
+                fbinfo[i][j] = fbinfo[i][j].getText()
+
+        returnValue({
+            &quot;result&quot;: &quot;ok&quot;,
+            &quot;fbresults&quot;: fbinfo,
+            &quot;matchtotal&quot;: matchtotal,
+        })
+
+
</ins><span class="cx">     @staticmethod
</span><del>-    def _transform_string(value, ownerHomeChild, objectResource):
</del><ins>+    def _result_string(value, shareeView, objectResource):
</ins><span class="cx">         return str(value)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @staticmethod
</span><del>-    def _transform_externalize(value, ownerHomeChild, objectResource):
-        if isinstance(value, ownerHomeChild._objectResourceClass):
</del><ins>+    def _result_externalize(value, shareeView, objectResource):
+        if isinstance(value, shareeView._objectResourceClass):
</ins><span class="cx">             value = value.externalize()
</span><span class="cx">         elif value is not None:
</span><span class="cx">             value = [v.externalize() for v in value]
</span><span class="lines">@@ -528,14 +667,34 @@
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><span class="cx">     def _make_simple_homechild_action(cls, action, method):
</span><del>-        setattr(cls, &quot;send_{}&quot;.format(action), lambda self, shareeView, *args, **kwargs: self._simple_send(action, shareeView, args=args, kwargs=kwargs))
-        setattr(cls, &quot;recv_{}&quot;.format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method))
</del><ins>+        setattr(
+            cls,
+            &quot;send_{}&quot;.format(action),
+            lambda self, shareeView, *args, **kwargs:
+                self._simple_send(action, shareeView, args=args, kwargs=kwargs)
+        )
+        setattr(
+            cls,
+            &quot;recv_{}&quot;.format(action),
+            lambda self, txn, message:
+                self._simple_recv(txn, action, message, method)
+        )
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><span class="cx">     def _make_simple_object_action(cls, action, method, transform_result=None):
</span><del>-        setattr(cls, &quot;send_{}&quot;.format(action), lambda self, shareeView, objectResource, *args, **kwargs: self._simple_send(action, shareeView, objectResource, args=args, kwargs=kwargs))
-        setattr(cls, &quot;recv_{}&quot;.format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_result))
</del><ins>+        setattr(
+            cls,
+            &quot;send_{}&quot;.format(action),
+            lambda self, shareeView, objectResource, *args, **kwargs:
+                self._simple_send(action, shareeView, objectResource, args=args, kwargs=kwargs)
+        )
+        setattr(
+            cls,
+            &quot;recv_{}&quot;.format(action),
+            lambda self, txn, message:
+                self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_result)
+        )
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> # Calls on L{CommonHomeChild} objects
</span><span class="lines">@@ -547,10 +706,10 @@
</span><span class="cx"> PoddingConduit._make_simple_homechild_action(&quot;resourcenameforuid&quot;, &quot;resourceNameForUID&quot;)
</span><span class="cx"> 
</span><span class="cx"> # Calls on L{CommonObjectResource} objects
</span><del>-PoddingConduit._make_simple_object_action(&quot;loadallobjects&quot;, &quot;loadAllObjects&quot;, transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action(&quot;loadallobjectswithnames&quot;, &quot;loadAllObjectsWithNames&quot;, transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action(&quot;objectwith&quot;, &quot;objectWith&quot;, transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action(&quot;create&quot;, &quot;create&quot;, transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action(&quot;setcomponent&quot;, &quot;setComponentText&quot;)
-PoddingConduit._make_simple_object_action(&quot;component&quot;, &quot;component&quot;, transform_result=PoddingConduit._transform_string)
</del><ins>+PoddingConduit._make_simple_object_action(&quot;loadallobjects&quot;, &quot;loadAllObjects&quot;, transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action(&quot;loadallobjectswithnames&quot;, &quot;loadAllObjectsWithNames&quot;, transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action(&quot;objectwith&quot;, &quot;objectWith&quot;, transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action(&quot;create&quot;, &quot;create&quot;, transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action(&quot;setcomponent&quot;, &quot;setComponent&quot;)
+PoddingConduit._make_simple_object_action(&quot;component&quot;, &quot;component&quot;, transform_result=PoddingConduit._result_string)
</ins><span class="cx"> PoddingConduit._make_simple_object_action(&quot;remove&quot;, &quot;remove&quot;)
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingresourcepy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -139,6 +139,11 @@
</span><span class="cx">             self.log.error(&quot;Invalid JSON data in request: {ex}\n{body}&quot;, ex=e, body=body)
</span><span class="cx">             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, &quot;Invalid JSON data in request: {}\n{}&quot;.format(e, body)))
</span><span class="cx"> 
</span><ins>+        # Log extended item
+        if not hasattr(request, &quot;extendedLogItems&quot;):
+            request.extendedLogItems = {}
+        request.extendedLogItems[&quot;xpod&quot;] = j[&quot;action&quot;] if &quot;action&quot; in j else &quot;unknown&quot;
+
</ins><span class="cx">         # Get the conduit to process the data
</span><span class="cx">         try:
</span><span class="cx">             result = yield self.store.conduit.processRequest(j)
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingtesttest_conduitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -32,6 +32,9 @@
</span><span class="cx"> from twistedcaldav.ical import Component, normalize_iCalStr
</span><span class="cx"> from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError, \
</span><span class="cx">     ObjectResourceNameNotAllowedError
</span><ins>+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from twistedcaldav.caldavxml import TimeRange
+from pycalendar.period import Period
</ins><span class="cx"> 
</span><span class="cx"> class TestConduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
</span><span class="cx"> 
</span><span class="lines">@@ -770,6 +773,7 @@
</span><span class="cx">         self.assertTrue(resource is not None)
</span><span class="cx">         self.assertEqual(resource.name(), &quot;1.ics&quot;)
</span><span class="cx">         self.assertEqual(resource.uid(), &quot;uid1&quot;)
</span><ins>+        self.assertFalse(resource._componentChanged)
</ins><span class="cx">         yield self.otherCommit()
</span><span class="cx"> 
</span><span class="cx">         shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home=&quot;puser01&quot;, name=&quot;shared-calendar&quot;)
</span><span class="lines">@@ -884,3 +888,42 @@
</span><span class="cx">         object1 = yield self.calendarObjectUnderTest(home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;1.ics&quot;)
</span><span class="cx">         self.assertTrue(object1 is None)
</span><span class="cx">         yield self.commit()
</span><ins>+
+
+    @inlineCallbacks
+    def test_freebusy(self):
+        &quot;&quot;&quot;
+        Test that action=component works.
+        &quot;&quot;&quot;
+
+        yield self.createShare(&quot;user01&quot;, &quot;puser01&quot;)
+
+        calendar1 = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        yield  calendar1.createCalendarObjectWithName(&quot;1.ics&quot;, Component.fromString(self.caldata1))
+        yield self.commit()
+
+        fbstart = &quot;{now:04d}0102T000000Z&quot;.format(**self.nowYear)
+        fbend = &quot;{now:04d}0103T000000Z&quot;.format(**self.nowYear)
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home=&quot;puser01&quot;, name=&quot;shared-calendar&quot;)
+
+        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(&quot;{now:04d}0102T140000Z/PT1H&quot;.format(**self.nowYear)), ])
+        self.assertEqual(len(fbinfo[1]), 0)
+        self.assertEqual(len(fbinfo[2]), 0)
+        yield self.otherCommit()
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastorepoddingtestutilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -26,6 +26,8 @@
</span><span class="cx"> 
</span><span class="cx"> import twext.web2.dav.test.util
</span><span class="cx"> from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
</span><ins>+from twext.enterprise.ienterprise import AlreadyFinishedError
+import json
</ins><span class="cx"> 
</span><span class="cx"> class FakeConduitRequest(object):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="lines">@@ -52,7 +54,7 @@
</span><span class="cx">     def __init__(self, server, data):
</span><span class="cx"> 
</span><span class="cx">         self.server = server
</span><del>-        self.data = data
</del><ins>+        self.data = json.dumps(data)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -61,6 +63,7 @@
</span><span class="cx">         # Generate an HTTP client request
</span><span class="cx">         try:
</span><span class="cx">             response = (yield self._processRequest())
</span><ins>+            response = json.loads(response)
</ins><span class="cx">         except Exception as e:
</span><span class="cx">             raise ValueError(&quot;Failed cross-pod request: {}&quot;.format(e))
</span><span class="cx"> 
</span><span class="lines">@@ -77,7 +80,8 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         store = self.storeMap[self.server.details()]
</span><del>-        result = yield store.conduit.processRequest(self.data)
</del><ins>+        result = yield store.conduit.processRequest(json.loads(self.data))
+        result = json.dumps(result)
</ins><span class="cx">         returnValue(result)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -121,10 +125,24 @@
</span><span class="cx">     def newOtherTransaction(self):
</span><span class="cx">         assert self.otherTransaction is None
</span><span class="cx">         store2 = self.otherStoreUnderTest()
</span><del>-        self.otherTransaction = store2.newTransaction()
</del><ins>+        txn = store2.newTransaction()
+        @inlineCallbacks
+        def maybeCommitThis():
+            try:
+                yield txn.commit()
+            except AlreadyFinishedError:
+                pass
+        self.addCleanup(maybeCommitThis)
+        self.otherTransaction = txn
</ins><span class="cx">         return self.otherTransaction
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def otherTransactionUnderTest(self):
+        if self.otherTransaction is None:
+            self.newOtherTransaction()
+        return self.otherTransaction
+
+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def otherCommit(self):
</span><span class="cx">         assert self.otherTransaction is not None
</span><span class="lines">@@ -190,13 +208,24 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def createShare(self, ownerGUID, shareeGUID, name=&quot;calendar&quot;):
</del><ins>+    def createShare(self, ownerGUID=&quot;user01&quot;, shareeGUID=&quot;puser02&quot;, name=&quot;calendar&quot;):
</ins><span class="cx"> 
</span><span class="cx">         home = yield self.homeUnderTest(name=ownerGUID, create=True)
</span><del>-        calendar = yield home.calendarWithName(&quot;calendar&quot;)
</del><ins>+        calendar = yield home.calendarWithName(name)
</ins><span class="cx">         yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, &quot;shared&quot;, shareName=&quot;shared-calendar&quot;)
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
</span><span class="cx">         yield home2.acceptShare(&quot;shared-calendar&quot;)
</span><span class="cx">         yield self.otherCommit()
</span><ins>+
+        returnValue(&quot;shared-calendar&quot;)
+
+
+    @inlineCallbacks
+    def removeShare(self, ownerGUID=&quot;user01&quot;, shareeGUID=&quot;puser02&quot;, name=&quot;calendar&quot;):
+
+        home = yield self.homeUnderTest(name=ownerGUID)
+        calendar = yield home.calendarWithName(name)
+        yield calendar.uninviteUserFromShare(shareeGUID)
+        yield self.commit()
</ins></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -85,6 +85,7 @@
</span><span class="cx"> from zope.interface import implements, directlyProvides
</span><span class="cx"> 
</span><span class="cx"> from collections import namedtuple
</span><ins>+import itertools
</ins><span class="cx"> import json
</span><span class="cx"> import sys
</span><span class="cx"> import time
</span><span class="lines">@@ -1471,7 +1472,7 @@
</span><span class="cx">         if shareeView is not None:
</span><span class="cx">             yield shareeView.declineShare()
</span><span class="cx"> 
</span><del>-        returnValue(shareeView)
</del><ins>+        returnValue(shareeView is not None)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     #
</span><span class="lines">@@ -1491,12 +1492,30 @@
</span><span class="cx">         # Try to find owner calendar via its external id
</span><span class="cx">         ownerView = yield ownerHome.childWithExternalID(ownerRID)
</span><span class="cx">         if ownerView is None:
</span><del>-            ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
</del><ins>+            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(&quot;External invite collection name is present with a different externalID and still has shares&quot;)
+                    raise
+                log.error(&quot;External invite collection name is present with a different externalID - trying to fix&quot;)
+                yield ownerHome.removeExternalChild(oldOwnerView)
+                ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+
</ins><span class="cx">             if supported_components is not None and hasattr(ownerView, &quot;setSupportedComponents&quot;):
</span><span class="cx">                 yield ownerView.setSupportedComponents(supported_components)
</span><span class="cx"> 
</span><span class="cx">         # Now carry out the share operation
</span><del>-        yield ownerView.inviteUserToShare(self.uid(), bindMode, summary, shareName=shareUID)
</del><ins>+        if bindMode == _BIND_MODE_DIRECT:
+            yield ownerView.directShareWithUser(self.uid(), shareName=shareUID)
+        else:
+            yield ownerView.inviteUserToShare(self.uid(), bindMode, summary, shareName=shareUID)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -1518,7 +1537,12 @@
</span><span class="cx">         # Now carry out the share operation
</span><span class="cx">         yield ownerView.uninviteUserFromShare(self.uid())
</span><span class="cx"> 
</span><ins>+        # 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)
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def processExternalReply(self, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -1544,7 +1568,29 @@
</span><span class="cx">             yield shareeHome.declineShare(shareUID)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def processExternalRemove(self, ownerUID, shareeUID, shareUID):
+        &quot;&quot;&quot;
+        External invite received.
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><ins>+        # 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(&quot;Invalid sharee UID: {}&quot;.format(shareeUID))
+
+        # Try to find owner calendar via its external id
+        shareeView = yield shareeHome.anyObjectWithShareUID(shareUID)
+        if shareeView is None:
+            raise ExternalShareFailed(&quot;Invalid share UID: {}&quot;.format(shareUID))
+
+        # Now carry out the share operation
+        yield shareeView.deleteShare()
+
+
+
</ins><span class="cx"> class CommonHome(SharingHomeMixIn):
</span><span class="cx">     log = Logger()
</span><span class="cx"> 
</span><span class="lines">@@ -2158,6 +2204,10 @@
</span><span class="cx">         record a revision for the sharee home and sharee collection name with the &quot;deleted&quot; flag set. That way
</span><span class="cx">         the shared collection can be reported as removed.
</span><span class="cx"> 
</span><ins>+        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.
+
</ins><span class="cx">         @param revision: the sync revision to compare to
</span><span class="cx">         @type revision: C{str}
</span><span class="cx">         @param depth: depth for determine what changed
</span><span class="lines">@@ -2176,6 +2226,7 @@
</span><span class="cx"> 
</span><span class="cx">         changed = set()
</span><span class="cx">         deleted = set()
</span><ins>+        invalid = set()
</ins><span class="cx">         deleted_collections = set()
</span><span class="cx">         changed_collections = set()
</span><span class="cx">         for path, name, wasdeleted in results:
</span><span class="lines">@@ -2211,40 +2262,52 @@
</span><span class="cx">         shares = yield self.children()
</span><span class="cx">         for share in shares:
</span><span class="cx">             if not share.owned():
</span><del>-                sharerevision = 0 if revision &lt; share._bindRevision else revision
-                results = [
-                    (
-                        share.name(),
-                        name if name else &quot;&quot;,
-                        wasdeleted
-                    )
-                    for name, wasdeleted in
-                    (yield Select([rev.RESOURCE_NAME, rev.DELETED],
-                                     From=rev,
-                                    Where=(rev.REVISION &gt; sharerevision).And(
-                                    rev.RESOURCE_ID == share._resourceID)).on(self._txn))
-                    if name
-                ]
</del><ins>+                if share.external():
+                    if depth == &quot;1&quot;:
+                        pass
+                    else:
+                        name = share.name() + &quot;/&quot;
+                        invalid.add(name)
+                        if name in changed:
+                            changed.remove(name)
+                        if name in deleted:
+                            deleted.remove(name)
+                else:
+                    sharerevision = 0 if revision &lt; share._bindRevision else revision
+                    results = [
+                        (
+                            share.name(),
+                            name if name else &quot;&quot;,
+                            wasdeleted
+                        )
+                        for name, wasdeleted in
+                        (yield Select([rev.RESOURCE_NAME, rev.DELETED],
+                                         From=rev,
+                                        Where=(rev.REVISION &gt; sharerevision).And(
+                                        rev.RESOURCE_ID == share._resourceID)).on(self._txn))
+                        if name
+                    ]
</ins><span class="cx"> 
</span><del>-                for path, name, wasdeleted in results:
-                    if wasdeleted:
-                        if sharerevision:
-                            if depth == &quot;1&quot;:
-                                changed.add(&quot;%s/&quot; % (path,))
-                            else:
-                                deleted.add(&quot;%s/%s&quot; % (path, name,))
</del><ins>+                    for path, name, wasdeleted in results:
+                        if wasdeleted:
+                            if sharerevision:
+                                if depth == &quot;1&quot;:
+                                    changed.add(&quot;%s/&quot; % (path,))
+                                else:
+                                    deleted.add(&quot;%s/%s&quot; % (path, name,))
</ins><span class="cx"> 
</span><del>-                for path, name, wasdeleted in results:
-                    # Always report collection as changed
-                    changed.add(&quot;%s/&quot; % (path,))
-                    if name:
-                        # Resource changed - for depth &quot;infinity&quot; report resource as changed
-                        if depth != &quot;1&quot;:
-                            changed.add(&quot;%s/%s&quot; % (path, name,))
</del><ins>+                    for path, name, wasdeleted in results:
+                        # Always report collection as changed
+                        changed.add(&quot;%s/&quot; % (path,))
+                        if name:
+                            # Resource changed - for depth &quot;infinity&quot; report resource as changed
+                            if depth != &quot;1&quot;:
+                                changed.add(&quot;%s/%s&quot; % (path, name,))
</ins><span class="cx"> 
</span><span class="cx">         changed = sorted(changed)
</span><span class="cx">         deleted = sorted(deleted)
</span><del>-        returnValue((changed, deleted))
</del><ins>+        invalid = sorted(invalid)
+        returnValue((changed, deleted, invalid,))
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -3084,6 +3147,11 @@
</span><span class="cx">         if shareeView is None:
</span><span class="cx">             shareeView = yield self.createShare(shareeUID=shareeUID, mode=_BIND_MODE_DIRECT, shareName=shareName)
</span><span class="cx">             yield shareeView.newShare()
</span><ins>+
+            # Check for external
+            if shareeView.viewerHome().external():
+                yield self._sendExternalInvite(shareeView)
+
</ins><span class="cx">         returnValue(shareeView)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -3154,6 +3222,8 @@
</span><span class="cx">         ownerView = yield self.ownerView()
</span><span class="cx">         if self.direct():
</span><span class="cx">             yield ownerView.removeShare(self)
</span><ins>+            if not ownerView.external():
+                yield self._removeExternalInvite(ownerView)
</ins><span class="cx">         else:
</span><span class="cx">             yield self.declineShare()
</span><span class="cx"> 
</span><span class="lines">@@ -3303,6 +3373,18 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def _removeExternalInvite(self):
+
+        yield self._txn.store().conduit.send_shareremove(
+            self._txn,
+            self.viewerHome()._homeType,
+            self.ownerHome().uid(),
+            self.viewerHome().uid(),
+            self.shareName(),
+        )
+
+
</ins><span class="cx">     #
</span><span class="cx">     # Lower level API
</span><span class="cx">     #
</span><span class="lines">@@ -4695,7 +4777,7 @@
</span><span class="cx">         @type revision: C{int}
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        if revision &lt; self._bindRevision:
</del><ins>+        if revision &lt; self._bindRevision and not self.external():
</ins><span class="cx">             revision = 0
</span><span class="cx">         return super(CommonHomeChild, self).resourceNamesSinceRevision(revision)
</span><span class="cx"> 
</span><span class="lines">@@ -5225,17 +5307,25 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @classmethod
+    def _otherSerializedAttributes(cls): #@NoSelf
+        return (
+            &quot;_componentChanged&quot;,
+        )
+
+
</ins><span class="cx">     def externalize(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Create a dictionary mapping key attributes so this object can be sent over a cross-pod call
</span><span class="cx">         and reconstituted at the other end. Note that the other end may have a different schema so
</span><span class="cx">         the attributes may not match exactly and will need to be processed accordingly.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return dict([(attr[1:], getattr(self, attr)) for attr in self._rowAttributes()])
</del><ins>+        return dict([(attr[1:], getattr(self, attr, None)) for attr in itertools.chain(self._rowAttributes(), self._otherSerializedAttributes())])
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><del>-    def internalize(cls, mapping):
</del><ins>+    @inlineCallbacks
+    def internalize(cls, parent, mapping):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Given a mapping generated by L{externalize}, convert the values into an array of database
</span><span class="cx">         like items that conforms to the ordering of L{_allColumns} so it can be fed into L{makeClass}.
</span><span class="lines">@@ -5243,7 +5333,10 @@
</span><span class="cx">         C{None} and ignore extra items.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        return [mapping.get(row[1:]) for row in cls._rowAttributes()]
</del><ins>+        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)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -5356,18 +5449,10 @@
</span><span class="cx">         self._locked = True
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def setComponent(self, component, inserting=False, options=None):
</del><ins>+    def setComponent(self, component, inserting=False):
</ins><span class="cx">         raise NotImplementedError
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def setComponentText(self, component_text, inserting=False, options=None):
-        &quot;&quot;&quot;
-        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.
-        &quot;&quot;&quot;
-        return self.setComponent(self._componentClass.fromString(component_text), inserting, options)
-
-
</del><span class="cx">     def component(self):
</span><span class="cx">         raise NotImplementedError
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdaboocrosspodsharingtxdavcommondatastoresql_externalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py (12110 => 12111)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -14,21 +14,21 @@
</span><span class="cx"> # See the License for the specific language governing permissions and
</span><span class="cx"> # limitations under the License.
</span><span class="cx"> ##
</span><del>-from txdav.common.icommondatastore import NonExistentExternalShare, \
-    ExternalShareFailed
</del><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> SQL data store.
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-
</del><span class="cx"> from twext.internet.decorate import memoizedKey
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> 
</span><ins>+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
</ins><span class="cx"> from txdav.base.propertystore.sql import PropertyStore
</span><span class="cx"> from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
</span><span class="cx">     CommonObjectResource
</span><span class="cx"> from txdav.common.datastore.sql_tables import _HOME_STATUS_EXTERNAL
</span><ins>+from txdav.common.icommondatastore import NonExistentExternalShare, \
+    ExternalShareFailed
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> log = Logger()
</span><span class="lines">@@ -116,6 +116,17 @@
</span><span class="cx">         raise AssertionError(&quot;CommonHomeExternal: not supported&quot;)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def removeExternalChild(self, child):
+        &quot;&quot;&quot;
+        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.
+        &quot;&quot;&quot;
+        if child._externalID is None:
+            raise AssertionError(&quot;CommonHomeExternal: not supported&quot;)
+        yield super(CommonHomeExternal, self).removeChildWithName(child.name())
+
+
</ins><span class="cx">     def syncToken(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         No children.
</span><span class="lines">@@ -217,11 +228,16 @@
</span><span class="cx">         yield ownerView.removeShare(self)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def remove(self, rid):
</del><ins>+    @inlineCallbacks
+    def remove(self):
</ins><span class="cx">         &quot;&quot;&quot;
</span><del>-        External shares are never removed directly - instead they must be &quot;uninvited&quot;.
</del><ins>+        External shares are never removed directly - instead they must be &quot;uninvited&quot;. However,
+        the owner's external calendar can be removed.
</ins><span class="cx">         &quot;&quot;&quot;
</span><del>-        raise AssertionError(&quot;CommonHomeChildExternal: not supported&quot;)
</del><ins>+        if self.owned():
+            yield super(CommonHomeChildExternal, self).remove()
+        else:
+            raise AssertionError(&quot;CommonHomeChildExternal: not supported&quot;)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="lines">@@ -291,14 +307,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def createObjectResourceWithName(self, name, component, options=None):
-        &quot;&quot;&quot;
-        Actually I think we can defer this to the object resource class's .create()
-        &quot;&quot;&quot;
-        raise NotImplementedError(&quot;TODO: external resource&quot;)
-
-
-    @inlineCallbacks
</del><span class="cx">     def moveObjectResource(self, child, newparent, newname=None):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         The base class does an optimization to avoid removing/re-creating
</span><span class="lines">@@ -348,7 +356,7 @@
</span><span class="cx">         results = []
</span><span class="cx">         if mapping_list:
</span><span class="cx">             for mapping in mapping_list:
</span><del>-                child = yield cls.makeClass(parent, cls.internalize(mapping))
</del><ins>+                child = yield cls.internalize(parent, mapping)
</ins><span class="cx">                 results.append(child)
</span><span class="cx">         returnValue(results)
</span><span class="cx"> 
</span><span class="lines">@@ -361,7 +369,7 @@
</span><span class="cx">         results = []
</span><span class="cx">         if mapping_list:
</span><span class="cx">             for mapping in mapping_list:
</span><del>-                child = yield cls.makeClass(parent, cls.internalize(mapping))
</del><ins>+                child = yield cls.internalize(parent, mapping)
</ins><span class="cx">                 results.append(child)
</span><span class="cx">         returnValue(results)
</span><span class="cx"> 
</span><span class="lines">@@ -372,7 +380,7 @@
</span><span class="cx">         mapping = yield parent._txn.store().conduit.send_objectwith(parent, None, name, uid, resourceID)
</span><span class="cx"> 
</span><span class="cx">         if mapping:
</span><del>-            child = yield cls.makeClass(parent, cls.internalize(mapping))
</del><ins>+            child = yield cls.internalize(parent, mapping)
</ins><span class="cx">             returnValue(child)
</span><span class="cx">         else:
</span><span class="cx">             returnValue(None)
</span><span class="lines">@@ -381,20 +389,20 @@
</span><span class="cx">     @classmethod
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def create(cls, parent, name, component, options=None):
</span><del>-        mapping = yield parent._txn.store().conduit.send_create(parent, None, name, component, options=options)
</del><ins>+        mapping = yield parent._txn.store().conduit.send_create(parent, None, name, str(component), options=options)
</ins><span class="cx"> 
</span><span class="cx">         if mapping:
</span><del>-            child = yield cls.makeClass(parent, cls.internalize(mapping))
</del><ins>+            child = yield cls.internalize(parent, mapping)
</ins><span class="cx">             returnValue(child)
</span><span class="cx">         else:
</span><span class="cx">             returnValue(None)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def setComponent(self, component, inserting=False, options=None):
-        changed = yield self._txn.store().conduit.send_setcomponent(self.parentCollection(), self, str(component), inserting, options)
</del><ins>+    def setComponent(self, component, **kwargs):
+        self._componentChanged = yield self._txn.store().conduit.send_setcomponent(self.parentCollection(), self, str(component), **kwargs)
</ins><span class="cx">         self._cachedComponent = None
</span><del>-        returnValue(changed)
</del><ins>+        returnValue(self._componentChanged)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span></span></pre>
</div>
</div>

</body>
</html>