<!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, "invalid-share"),
+ "Invite UID not valid",
+ ))
</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, "invalid-share"),
+ "Invite UID not valid",
+ ))
</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("Removing shared collection %s" % (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("Deleting collection %s" % (self,))
</span><span class="lines">@@ -3371,6 +3373,8 @@
</span><span class="cx"> if self.isShareeResource():
</span><span class="cx"> log.debug("Removing shared resource %s" % (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"> """
</span><ins>+ Get freebusy information for a calendar. Different behavior for internal vs external calendars.
+
+ See L{_internalGenerateFreeBusyInfo} for argument description.
+ """
+
+ # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
+ # can be split across L{CalendarObject} and L{CalendarObjectExternal}
+ if calresource.external():
+ return _externalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid,
+ organizer,
+ organizerPrincipal,
+ same_calendar_user,
+ servertoserver,
+ event_details,
+ logItems
+ )
+ else:
+ return _internalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid,
+ organizer,
+ organizerPrincipal,
+ same_calendar_user,
+ servertoserver,
+ event_details,
+ logItems
+ )
+
+
+
+@inlineCallbacks
+def _externalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid=None,
+ organizer=None,
+ organizerPrincipal=None,
+ same_calendar_user=False,
+ servertoserver=False,
+ event_details=None,
+ logItems=None,
+):
+ """
+ Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
+ any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
+
+ See L{_internalGenerateFreeBusyInfo} for argument description.
+ """
+ fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid, organizer, organizerPrincipal, same_calendar_user, servertoserver, event_details)
+ for i in range(3):
+ fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
+ returnValue(matchtotal)
+
+
+
+@inlineCallbacks
+def _internalGenerateFreeBusyInfo(
+ calresource,
+ fbinfo,
+ timerange,
+ matchtotal,
+ excludeuid=None,
+ organizer=None,
+ organizerPrincipal=None,
+ same_calendar_user=False,
+ servertoserver=False,
+ event_details=None,
+ logItems=None,
+):
+ """
</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 != "text/calendar":
+ attrs["content-type"] = format
+
</ins><span class="cx"> if isinstance(calendar, str):
</span><span class="cx"> if not calendar:
</span><span class="cx"> raise ValueError("Missing calendar data")
</span><span class="cx"> return clazz(PCDATAElement(calendar))
</span><span class="cx"> elif isinstance(calendar, iComponent):
</span><span class="cx"> assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (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("Not a calendar: %s" % (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"> """
</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), "infinity")
</span><span class="cx">
</span><span class="cx"> self.assertEquals(set(changed), set(["calendar_1/",
</span><span class="lines">@@ -1610,11 +1610,13 @@
</span><span class="cx"> "calendar_1/2.ics",
</span><span class="cx"> "other-calendar/"]))
</span><span class="cx"> self.assertEquals(set(deleted), set(["calendar_1/2.ics"]))
</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), "infinity")
</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 "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from twisted.internet.defer import inlineCallbacks
+
+from twext.python.clsprop import classproperty
+from txdav.common.datastore.test.util import populateCalendarsFrom
+from txdav.common.datastore.sql_tables import _BIND_MODE_READ, \
+ _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest
+
+
+class BaseSharingTests(MultiStoreConduitTest):
+
+ """
+ Test store-based calendar sharing.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ yield super(BaseSharingTests, self).setUp()
+ yield self.populate()
+
+
+ @inlineCallbacks
+ def populate(self):
+ yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+ self.notifierFactory.reset()
+
+ cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:20131122T140000
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+"""
+
+ @classproperty(cache=False)
+ def requirements(cls): #@NoSelf
+ return {
+ "user01": {
+ "calendar": {
+ "cal1.ics": (cls.cal1, None,),
+ },
+ "inbox": {
+ },
+ },
+ "user02": {
+ "calendar": {
+ },
+ "inbox": {
+ },
+ },
+ "user03": {
+ "calendar": {
+ },
+ "inbox": {
+ },
+ },
+ }
+
+
+
+class CalendarSharing(BaseSharingTests):
+
+ @inlineCallbacks
+ def test_no_shares(self):
+ """
+ Test that initially there are no shares.
+ """
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_invite_sharee(self):
+ """
+ Test invite/uninvite creates/removes shares and notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+ self.assertEqual(invites[0].uid, shareeView.shareUID())
+ self.assertEqual(invites[0].ownerUID, "user01")
+ self.assertEqual(invites[0].shareeUID, "puser02")
+ self.assertEqual(invites[0].mode, _BIND_MODE_READ)
+ self.assertEqual(invites[0].status, _BIND_STATUS_INVITED)
+ self.assertEqual(invites[0].summary, "summary")
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID, ])
+ yield self.otherCommit()
+
+ # Uninvite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ yield calendar.uninviteUserFromShare("puser02")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [])
+ yield self.otherCommit()
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+ yield calendar.setShared(False)
+ self.assertFalse(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_accept_share(self):
+ """
+ Test that invite+accept creates shares and notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield self.otherTransactionUnderTest().notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ # Re-accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_decline_share(self):
+ """
+ Test that invite+decline does not create shares but does create notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield txn2.notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Decline
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.declineShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ # Redecline
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.declineShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_accept_decline_share(self):
+ """
+ Test that invite+accept/decline creates/removes shares and notifications.
+ Decline via the home.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+ inviteUID = shareeView.shareUID()
+
+ sharedName = shareeView.name()
+
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield txn2.notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+ yield self.commit()
+
+ # Decline
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.declineShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertTrue(calendar.isShared())
+
+
+ @inlineCallbacks
+ def test_accept_remove_share(self):
+ """
+ Test that invite+accept/decline creates/removes shares and notifications.
+ Decline via the shared collection (removal).
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+
+ inviteUID = shareeView.shareUID()
+ sharedName = shareeView.name()
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+
+ notifyHome = yield txn2.notificationsWithUID("puser02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 1)
+ yield self.otherCommit()
+
+ # Accept
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ yield shareeHome.acceptShare(inviteUID)
+
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is not None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+ yield self.commit()
+
+ # Delete
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ yield shared.deleteShare()
+ yield self.otherCommit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertTrue(shared is None)
+ yield self.otherCommit()
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(notifications, [inviteUID + "-reply", ])
+
+
+ @inlineCallbacks
+ def test_accept_remove_accept(self):
+ yield self.createShare()
+ yield self.removeShare()
+ shared_name = yield self.createShare()
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_accept_remove_accept_newcalendar(self):
+ """
+ Test that deleting and re-creating a share with the same sharer name works.
+ """
+
+ home = yield self.homeUnderTest(name="user01", create=True)
+ yield home.createCalendarWithName("shared")
+ yield self.commit()
+
+ shared_name = yield self.createShare(name="shared")
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+ yield self.removeShare(name="shared")
+ home = yield self.homeUnderTest(name="user01", create=True)
+ yield home.removeCalendarWithName("shared")
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is None)
+ yield self.otherCommit()
+
+ home = yield self.homeUnderTest(name="user01", create=True)
+ yield home.createCalendarWithName("shared")
+ yield self.commit()
+
+ shared_name = yield self.createShare(name="shared")
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+
+ @inlineCallbacks
+ def test_inviteProperties(self):
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar.setUsedForFreeBusy(True)
+ yield self.commit()
+
+ shared_name = yield self.createShare()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name)
+ self.assertFalse(shared.isUsedForFreeBusy())
+
+
+ @inlineCallbacks
+ def test_direct_sharee(self):
+ """
+ Test invite/uninvite creates/removes shares and notifications.
+ """
+
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+ self.assertFalse(calendar.isShared())
+
+ shareeView = yield calendar.directShareWithUser("puser02")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 1)
+ self.assertEqual(invites[0].uid, shareeView.shareUID())
+ self.assertEqual(invites[0].ownerUID, "user01")
+ self.assertEqual(invites[0].shareeUID, "puser02")
+ self.assertEqual(invites[0].mode, _BIND_MODE_DIRECT)
+ self.assertEqual(invites[0].status, _BIND_STATUS_ACCEPTED)
+
+ sharedName = shareeView.name()
+
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="user02", name=sharedName)
+ self.assertTrue(shared is not None)
+
+ notifyHome = yield txn2.notificationsWithUID("user02")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 0)
+ yield self.otherCommit()
+
+ # Remove
+ txn2 = self.newOtherTransaction()
+ shared = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ yield shared.deleteShare()
+ yield self.otherCommit()
+
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
+ notifications = yield notifyHome.listNotificationObjects()
+ self.assertEqual(len(notifications), 0)
+
+ test_direct_sharee.skip = True
+
+ @inlineCallbacks
+ def test_sharedNotifierID(self):
+ shared_name = yield self.createShare()
+
+ home = yield self.homeUnderTest(name="user01")
+ self.assertEquals(home.notifierID(), ("CalDAV", "user01",))
+ calendar = yield home.calendarWithName("calendar")
+ self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ home = yield self.homeUnderTest(txn=txn2, name="puser02")
+ self.assertEquals(home.notifierID(), ("CalDAV", "puser02",))
+ calendar = yield home.calendarWithName(shared_name)
+ self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
+
+
+ @inlineCallbacks
+ def test_sharedWithTwo(self):
+ shared_name1 = yield self.createShare(shareeGUID="puser02")
+ shared_name2 = yield self.createShare(shareeGUID="puser03")
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=shared_name1)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser03", name=shared_name2)
+ self.assertTrue(otherCal is not None)
+ yield self.otherCommit()
+
+
+
+class SharingRevisions(BaseSharingTests):
+ """
+ Test store-based sharing and interaction with revision table.
+ """
+
+ @inlineCallbacks
+ def test_shareWithRevision(self):
+ """
+ Verify that bindRevision on calendars and shared calendars has the correct value.
+ """
+ sharedName = yield self.createShare()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+ @inlineCallbacks
+ def test_updateShareRevision(self):
+ """
+ Verify that bindRevision on calendars and shared calendars has the correct value.
+ """
+ # Invite
+ calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+ invites = yield calendar.sharingInvites()
+ self.assertEqual(len(invites), 0)
+
+ shareeView = yield calendar.inviteUserToShare("puser02", _BIND_MODE_READ, "summary")
+ newCalName = shareeView.shareUID()
+ yield self.commit()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+ yield self.commit()
+
+ txn2 = self.newOtherTransaction()
+ otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ otherCal = yield otherHome.anyObjectWithShareUID(newCalName)
+ self.assertEqual(otherCal._bindRevision, 0)
+ yield self.otherCommit()
+
+ txn2 = self.newOtherTransaction()
+ shareeHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ shareeView = yield shareeHome.acceptShare(newCalName)
+ sharedName = shareeView.name()
+ yield self.otherCommit()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+
+ txn2 = self.newOtherTransaction()
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+ @inlineCallbacks
+ def test_sharedRevisions(self):
+ """
+ Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+ """
+ sharedName = yield self.createShare()
+
+ normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+ self.assertEqual(normalCal._bindRevision, 0)
+
+ txn2 = self.newOtherTransaction()
+ otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+ otherCal = yield self.calendarUnderTest(txn=txn2, home="puser02", name=sharedName)
+ self.assertNotEqual(otherCal._bindRevision, 0)
+
+ sync_token = yield otherCal.syncToken()
+ revision = otherCal.revisionFromToken(sync_token)
+
+ changed, deleted = yield otherCal.resourceNamesSinceRevision(revision - 1)
+ self.assertNotEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 0)
+
+ changed, deleted = yield otherCal.resourceNamesSinceRevision(revision)
+ self.assertEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 0)
+
+ sync_token = yield otherHome.syncToken()
+ revision = otherHome.revisionFromToken(sync_token)
+
+ for depth in ("1", "infinity",):
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision - 1, depth)
+ self.assertEqual(len(changed), 0 if depth == "infinity" else 1)
+ self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+ self.assertEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 0)
+ self.assertEqual(len(invalid), 1 if depth == "infinity" else 0)
+
+ yield self.otherCommit()
+
+ yield self.removeShare()
+
+ txn2 = self.newOtherTransaction()
+ otherHome = yield self.homeUnderTest(txn=txn2, name="puser02")
+
+ for depth in ("1", "infinity",):
+ changed, deleted, invalid = yield otherHome.resourceNamesSinceRevision(revision, depth)
+ self.assertEqual(len(changed), 0)
+ self.assertEqual(len(deleted), 1)
+ self.assertEqual(len(invalid), 0)
</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 ("1", "infinity",):
</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="user02")
</span><span class="cx"> for depth in ("1", "infinity",):
</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):
+ """
+ Send a sharing remove cross-pod message.
+
+ @param homeType: Type of home being shared.
+ @type homeType: C{int}
+ @param ownerUID: GUID of the sharer.
+ @type ownerUID: C{str}
+ @param shareeUID: GUID of the recipient
+ @type shareeUID: C{str}
+ @param shareUID: Resource/invite ID for recipient
+ @type shareUID: C{str}
+ """
+
+ _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+ action = {
+ "action": "shareremove",
+ "type": homeType,
+ "owner": ownerUID,
+ "sharee": shareeUID,
+ "share_id": shareUID,
+ }
+
+ result = yield self.sendRequest(txn, recipient, action)
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def recv_shareremove(self, txn, message):
+ """
+ Process a sharing remove cross-pod message. Message arguments as per L{send_shareremove}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ if message["action"] != "shareremove":
+ raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareremove".format(message["action"]))
+
+ # Create a share
+ ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
+ if ownerHome is None or ownerHome.external():
+ FailedCrossPodRequestError("Invalid owner UID specified")
+
+ try:
+ yield ownerHome.processExternalRemove(
+ message["owner"],
+ message["sharee"],
+ message["share_id"],
+ )
+ except ExternalShareFailed as e:
+ FailedCrossPodRequestError(str(e))
+
+ returnValue({
+ "result": "ok",
+ })
+
+
</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["action"] != expected_action:
</span><span class="cx"> raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
</span><span class="cx">
</span><del>- # Create a share
- ownerHome = yield txn.homeWithUID(message["type"], message["owner"], create=True)
</del><ins>+ # Get a share
+ ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
</ins><span class="cx"> if ownerHome is None or ownerHome.external():
</span><span class="cx"> FailedCrossPodRequestError("Invalid owner UID specified")
</span><span class="cx">
</span><del>- ownerHomeChild = yield ownerHome.childWithID(message["owner_id"])
- if ownerHomeChild is None:
- FailedCrossPodRequestError("Invalid owner shared resource specified")
</del><ins>+ shareeHome = yield txn.homeWithUID(message["type"], message["sharee"])
+ if shareeHome is None or not shareeHome.external():
+ FailedCrossPodRequestError("Invalid sharee UID specified")
</ins><span class="cx">
</span><ins>+ shareeView = yield shareeHome.childWithID(message["owner_id"])
+ if shareeView is None:
+ FailedCrossPodRequestError("Invalid shared resource specified")
+
</ins><span class="cx"> resourceID = message.get("resource_id", 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("Invalid owner shared object resource specified")
</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"> """
</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("arguments", ()), **message.get("keywords", {}))
</del><ins>+ value = yield getattr(shareeView, method)(*message.get("arguments", ()), **message.get("keywords", {}))
</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("arguments", ()), **message.get("keywords", {}))
</span><span class="cx"> else:
</span><span class="cx"> # classmethod call
</span><del>- value = yield getattr(ownerHomeChild._objectResourceClass, method)(ownerHomeChild, *message.get("arguments", ()), **message.get("keywords", {}))
</del><ins>+ value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *message.get("arguments", ()), **message.get("keywords", {}))
</ins><span class="cx"> except Exception as e:
</span><span class="cx"> returnValue({
</span><span class="cx"> "result": "exception",
</span><span class="cx"> "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
</span><span class="cx"> "message": 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"> "result": "ok",
</span><del>- "value": value,
</del><ins>+ "value": 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("freebusy", calresource)
+ action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
+ action["matchtotal"] = matchtotal
+ action["excludeuid"] = excludeuid
+ action["organizer"] = organizer
+ action["organizerPrincipal"] = organizerPrincipal
+ action["same_calendar_user"] = same_calendar_user
+ action["servertoserver"] = servertoserver
+ action["event_details"] = event_details
+ result = yield self.sendRequest(calresource._txn, recipient, action)
+ if result["result"] == "ok":
+ returnValue((result["fbresults"], result["matchtotal"],))
+ elif result["result"] == "exception":
+ raise namedClass(result["class"])(result["message"])
+
+
+ @inlineCallbacks
+ def recv_freebusy(self, txn, message):
+ """
+ Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
+
+ @param message: message arguments
+ @type message: C{dict}
+ """
+
+ shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
+ try:
+ # Operate on the L{CommonHomeChild}
+ fbinfo = [[], [], []]
+ matchtotal = yield generateFreeBusyInfo(
+ shareeView,
+ fbinfo,
+ TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
+ message["matchtotal"],
+ message["excludeuid"],
+ message["organizer"],
+ message["organizerPrincipal"],
+ message["same_calendar_user"],
+ message["servertoserver"],
+ message["event_details"],
+ logItems=None
+ )
+ except Exception as e:
+ returnValue({
+ "result": "exception",
+ "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+ "message": str(e),
+ })
+
+ for i in range(3):
+ for j in range(len(fbinfo[i])):
+ fbinfo[i][j] = fbinfo[i][j].getText()
+
+ returnValue({
+ "result": "ok",
+ "fbresults": fbinfo,
+ "matchtotal": matchtotal,
+ })
+
+
</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, "send_{}".format(action), lambda self, shareeView, *args, **kwargs: self._simple_send(action, shareeView, args=args, kwargs=kwargs))
- setattr(cls, "recv_{}".format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method))
</del><ins>+ setattr(
+ cls,
+ "send_{}".format(action),
+ lambda self, shareeView, *args, **kwargs:
+ self._simple_send(action, shareeView, args=args, kwargs=kwargs)
+ )
+ setattr(
+ cls,
+ "recv_{}".format(action),
+ lambda self, txn, message:
+ self._simple_recv(txn, action, message, method)
+ )
</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, "send_{}".format(action), lambda self, shareeView, objectResource, *args, **kwargs: self._simple_send(action, shareeView, objectResource, args=args, kwargs=kwargs))
- setattr(cls, "recv_{}".format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_result))
</del><ins>+ setattr(
+ cls,
+ "send_{}".format(action),
+ lambda self, shareeView, objectResource, *args, **kwargs:
+ self._simple_send(action, shareeView, objectResource, args=args, kwargs=kwargs)
+ )
+ setattr(
+ cls,
+ "recv_{}".format(action),
+ lambda self, txn, message:
+ self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_result)
+ )
</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("resourcenameforuid", "resourceNameForUID")
</span><span class="cx">
</span><span class="cx"> # Calls on L{CommonObjectResource} objects
</span><del>-PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action("create", "create", transform_result=PoddingConduit._transform_externalize)
-PoddingConduit._make_simple_object_action("setcomponent", "setComponentText")
-PoddingConduit._make_simple_object_action("component", "component", transform_result=PoddingConduit._transform_string)
</del><ins>+PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("create", "create", transform_result=PoddingConduit._result_externalize)
+PoddingConduit._make_simple_object_action("setcomponent", "setComponent")
+PoddingConduit._make_simple_object_action("component", "component", transform_result=PoddingConduit._result_string)
</ins><span class="cx"> PoddingConduit._make_simple_object_action("remove", "remove")
</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("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
</span><span class="cx"> raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
</span><span class="cx">
</span><ins>+ # Log extended item
+ if not hasattr(request, "extendedLogItems"):
+ request.extendedLogItems = {}
+ request.extendedLogItems["xpod"] = j["action"] if "action" in j else "unknown"
+
</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(), "1.ics")
</span><span class="cx"> self.assertEqual(resource.uid(), "uid1")
</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="puser01", name="shared-calendar")
</span><span class="lines">@@ -884,3 +888,42 @@
</span><span class="cx"> object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
</span><span class="cx"> self.assertTrue(object1 is None)
</span><span class="cx"> yield self.commit()
</span><ins>+
+
+ @inlineCallbacks
+ def test_freebusy(self):
+ """
+ Test that action=component works.
+ """
+
+ yield self.createShare("user01", "puser01")
+
+ calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+ yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+ yield self.commit()
+
+ fbstart = "{now:04d}0102T000000Z".format(**self.nowYear)
+ fbend = "{now:04d}0103T000000Z".format(**self.nowYear)
+
+ shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+
+ fbinfo = [[], [], []]
+ matchtotal = yield generateFreeBusyInfo(
+ shared,
+ fbinfo,
+ TimeRange(start=fbstart, end=fbend),
+ 0,
+ excludeuid=None,
+ organizer=None,
+ organizerPrincipal=None,
+ same_calendar_user=False,
+ servertoserver=False,
+ event_details=False,
+ logItems=None
+ )
+
+ self.assertEqual(matchtotal, 1)
+ self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
+ self.assertEqual(len(fbinfo[1]), 0)
+ self.assertEqual(len(fbinfo[2]), 0)
+ yield self.otherCommit()
</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"> """
</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("Failed cross-pod request: {}".format(e))
</span><span class="cx">
</span><span class="lines">@@ -77,7 +80,8 @@
</span><span class="cx"> """
</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="calendar"):
</del><ins>+ def createShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
</ins><span class="cx">
</span><span class="cx"> home = yield self.homeUnderTest(name=ownerGUID, create=True)
</span><del>- calendar = yield home.calendarWithName("calendar")
</del><ins>+ calendar = yield home.calendarWithName(name)
</ins><span class="cx"> yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
</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("shared-calendar")
</span><span class="cx"> yield self.otherCommit()
</span><ins>+
+ returnValue("shared-calendar")
+
+
+ @inlineCallbacks
+ def removeShare(self, ownerGUID="user01", shareeGUID="puser02", name="calendar"):
+
+ home = yield self.homeUnderTest(name=ownerGUID)
+ calendar = yield home.calendarWithName(name)
+ yield calendar.uninviteUserFromShare(shareeGUID)
+ yield self.commit()
</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("External invite collection name is present with a different externalID and still has shares")
+ raise
+ log.error("External invite collection name is present with a different externalID - trying to fix")
+ yield ownerHome.removeExternalChild(oldOwnerView)
+ ownerView = yield ownerHome.createChildWithName(ownerName, externalID=ownerRID)
+
</ins><span class="cx"> if supported_components is not None and hasattr(ownerView, "setSupportedComponents"):
</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"> """
</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):
+ """
+ External invite received.
+ """
</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("Invalid sharee UID: {}".format(shareeUID))
+
+ # Try to find owner calendar via its external id
+ shareeView = yield shareeHome.anyObjectWithShareUID(shareUID)
+ if shareeView is None:
+ raise ExternalShareFailed("Invalid share UID: {}".format(shareUID))
+
+ # Now carry out the share operation
+ yield shareeView.deleteShare()
+
+
+
</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 "deleted" 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 < share._bindRevision else revision
- results = [
- (
- share.name(),
- name if name else "",
- wasdeleted
- )
- for name, wasdeleted in
- (yield Select([rev.RESOURCE_NAME, rev.DELETED],
- From=rev,
- Where=(rev.REVISION > sharerevision).And(
- rev.RESOURCE_ID == share._resourceID)).on(self._txn))
- if name
- ]
</del><ins>+ if share.external():
+ if depth == "1":
+ pass
+ else:
+ name = share.name() + "/"
+ invalid.add(name)
+ if name in changed:
+ changed.remove(name)
+ if name in deleted:
+ deleted.remove(name)
+ else:
+ sharerevision = 0 if revision < share._bindRevision else revision
+ results = [
+ (
+ share.name(),
+ name if name else "",
+ wasdeleted
+ )
+ for name, wasdeleted in
+ (yield Select([rev.RESOURCE_NAME, rev.DELETED],
+ From=rev,
+ Where=(rev.REVISION > sharerevision).And(
+ rev.RESOURCE_ID == share._resourceID)).on(self._txn))
+ if name
+ ]
</ins><span class="cx">
</span><del>- for path, name, wasdeleted in results:
- if wasdeleted:
- if sharerevision:
- if depth == "1":
- changed.add("%s/" % (path,))
- else:
- deleted.add("%s/%s" % (path, name,))
</del><ins>+ for path, name, wasdeleted in results:
+ if wasdeleted:
+ if sharerevision:
+ if depth == "1":
+ changed.add("%s/" % (path,))
+ else:
+ deleted.add("%s/%s" % (path, name,))
</ins><span class="cx">
</span><del>- for path, name, wasdeleted in results:
- # Always report collection as changed
- changed.add("%s/" % (path,))
- if name:
- # Resource changed - for depth "infinity" report resource as changed
- if depth != "1":
- changed.add("%s/%s" % (path, name,))
</del><ins>+ for path, name, wasdeleted in results:
+ # Always report collection as changed
+ changed.add("%s/" % (path,))
+ if name:
+ # Resource changed - for depth "infinity" report resource as changed
+ if depth != "1":
+ changed.add("%s/%s" % (path, name,))
</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"> """
</span><span class="cx">
</span><del>- if revision < self._bindRevision:
</del><ins>+ if revision < 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 (
+ "_componentChanged",
+ )
+
+
</ins><span class="cx"> def externalize(self):
</span><span class="cx"> """
</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"> """
</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"> """
</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"> """
</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):
- """
- This api is needed for cross-pod calls where the component is serialized as a str and we need
- to convert it back to the actual component class.
- """
- return self.setComponent(self._componentClass.fromString(component_text), inserting, options)
-
-
</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"> """
</span><span class="cx"> SQL data store.
</span><span class="cx"> """
</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("CommonHomeExternal: not supported")
</span><span class="cx">
</span><span class="cx">
</span><ins>+ @inlineCallbacks
+ def removeExternalChild(self, child):
+ """
+ Remove an external child. Check that it is invalid or unused before calling this because if there
+ are valid references to it, removing will break things.
+ """
+ if child._externalID is None:
+ raise AssertionError("CommonHomeExternal: not supported")
+ yield super(CommonHomeExternal, self).removeChildWithName(child.name())
+
+
</ins><span class="cx"> def syncToken(self):
</span><span class="cx"> """
</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"> """
</span><del>- External shares are never removed directly - instead they must be "uninvited".
</del><ins>+ External shares are never removed directly - instead they must be "uninvited". However,
+ the owner's external calendar can be removed.
</ins><span class="cx"> """
</span><del>- raise AssertionError("CommonHomeChildExternal: not supported")
</del><ins>+ if self.owned():
+ yield super(CommonHomeChildExternal, self).remove()
+ else:
+ raise AssertionError("CommonHomeChildExternal: not supported")
</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):
- """
- Actually I think we can defer this to the object resource class's .create()
- """
- raise NotImplementedError("TODO: external resource")
-
-
- @inlineCallbacks
</del><span class="cx"> def moveObjectResource(self, child, newparent, newname=None):
</span><span class="cx"> """
</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>