[CalendarServer-changes] [12007] CalendarServer/branches/users/cdaboo/sharing-in-the-store

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:24:48 PDT 2014


Revision: 12007
          http://trac.calendarserver.org//changeset/12007
Author:   cdaboo at apple.com
Date:     2013-11-23 13:41:29 -0800 (Sat, 23 Nov 2013)
Log Message:
-----------
Completed sharing-in-the-store refactoring.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/calendarserver/push/test/test_notifier.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/sharing.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/test/test_sharing.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql_sharing.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql_sharing.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_28_to_29.sql
    CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_28_to_29.sql

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/calendarserver/push/test/test_notifier.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/calendarserver/push/test/test_notifier.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/calendarserver/push/test/test_notifier.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -171,6 +171,7 @@
             [("/CalDAV/localhost/bar/", PushPriority.high)])
 
 
+
 class NotifierFactory(StoreTestCase):
 
     requirements = {
@@ -223,27 +224,27 @@
     def test_shareWithNotifier(self):
 
         calendar = yield self.calendarUnderTest()
-        home2 = yield self.homeUnderTest(name="home2")
-        yield calendar.shareWith(home2, _BIND_MODE_WRITE)
+        yield calendar.inviteUserToShare("home2", _BIND_MODE_WRITE, "")
         self.assertEquals(
             set(self.notifierFactory.history),
             set([
                 ("/CalDAV/example.com/home1/", PushPriority.high),
                 ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high),
                 ("/CalDAV/example.com/home2/", PushPriority.high),
+                ("/CalDAV/example.com/home2/notification/", PushPriority.high),
             ])
         )
         yield self.commit()
 
         calendar = yield self.calendarUnderTest()
-        home2 = yield self.homeUnderTest(name="home2")
-        yield calendar.unshareWith(home2)
+        yield calendar.uninviteUserFromShare("home2")
         self.assertEquals(
             set(self.notifierFactory.history),
             set([
                 ("/CalDAV/example.com/home1/", PushPriority.high),
                 ("/CalDAV/example.com/home1/calendar_1/", PushPriority.high),
                 ("/CalDAV/example.com/home2/", PushPriority.high),
+                ("/CalDAV/example.com/home2/notification/", PushPriority.high),
             ])
         )
         yield self.commit()
@@ -253,8 +254,9 @@
     def test_sharedCalendarNotifier(self):
 
         calendar = yield self.calendarUnderTest()
-        home2 = yield self.homeUnderTest(name="home2")
-        shareName = yield calendar.shareWith(home2, _BIND_MODE_WRITE)
+        shareeView = yield calendar.inviteUserToShare("home2", _BIND_MODE_WRITE, "")
+        yield shareeView.acceptShare("")
+        shareName = shareeView.name()
         yield self.commit()
         self.notifierFactory.reset()
 

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/sharing.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/sharing.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -34,7 +34,8 @@
 from txdav.common.datastore.sql_tables import _BIND_MODE_OWN, \
     _BIND_MODE_READ, _BIND_MODE_WRITE, _BIND_STATUS_INVITED, \
     _BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, \
-    _BIND_STATUS_INVALID, _ABO_KIND_GROUP, _BIND_STATUS_DELETED
+    _BIND_STATUS_INVALID, _ABO_KIND_GROUP, _BIND_STATUS_DELETED, \
+    _BIND_MODE_DIRECT, _BIND_MODE_INDIRECT
 from txdav.xml import element
 
 from twisted.internet.defer import succeed, inlineCallbacks, DeferredList, \
@@ -235,7 +236,8 @@
         return (
             hasattr(self, "_newStoreObject") and
             hasattr(self._newStoreObject, "owned") and
-            not self._newStoreObject.owned()
+            not self._newStoreObject.owned() and
+            getattr(self._newStoreObject, "_bindMode", None) is not None
         )
 
 
@@ -764,12 +766,13 @@
 }
 invitationBindModeFromXMLMap = dict((v, k) for k, v in invitationBindModeToXMLMap.iteritems())
 
-invitationAccessToBindModeMap = {
-    "own": _BIND_MODE_OWN,
-    "read-only": _BIND_MODE_READ,
-    "read-write": _BIND_MODE_WRITE,
-    }
-invitationAccessFromBindModeMap = dict((v, k) for k, v in invitationAccessToBindModeMap.iteritems())
+invitationAccessFromBindModeMap = {
+    _BIND_MODE_OWN: "own",
+    _BIND_MODE_READ: "read-only",
+    _BIND_MODE_WRITE: "read-write",
+    _BIND_MODE_DIRECT: "read-write",
+    _BIND_MODE_INDIRECT: "read-write",
+}
 
 
 class SharedHomeMixin(LinkFollowerMixIn):
@@ -804,7 +807,7 @@
         shareeView = yield self._newStoreHome.acceptShare(inviteUID, summary)
 
         # Return the URL of the shared collection
-        sharedAsURL = joinURL(self.url(), shareeView.name())
+        sharedAsURL = joinURL(self.url(), shareeView.shareName())
         returnValue(XMLResponse(
             code=responsecode.OK,
             element=customxml.SharedAs(

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/storebridge.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/storebridge.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -3116,23 +3116,6 @@
 
 
     @inlineCallbacks
-    def storeRemove(self, request):
-        """
-        handle remove of partially shared addressbook, else call super
-        """
-        if self.isShareeResource() and self._newStoreObject.shareUID() is None:
-            log.debug("Removing shared collection %s" % (self,))
-            for childname in (yield self.listChildren()):
-                child = (yield request.locateChildResource(self, childname))
-                if child.isShareeResource():
-                    yield child.storeRemove(request)
-
-            returnValue(NO_CONTENT)
-
-        returnValue((yield super(AddressBookCollectionResource, self).storeRemove(request)))
-
-
-    @inlineCallbacks
     def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses, format):
         """
         bulk create allowing groups to contain member UIDs added during the same bulk create

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/test/test_sharing.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/twistedcaldav/test/test_sharing.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -19,7 +19,7 @@
 from twext.web2.http_headers import MimeType
 from twext.web2.iweb import IResponse
 
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav import customxml
 from twistedcaldav import sharing
@@ -104,6 +104,10 @@
         returnValue(b)
 
 
+    def calendarHomeURLs(self):
+        return (self.homepath,)
+
+
     def principalURL(self):
         return self.path
 
@@ -141,22 +145,6 @@
             return c
 
         @patched
-        def sendInviteNotification(resourceSelf, record, request):
-            """
-            For testing purposes, sending an invite notification succeeds
-            without doing anything.
-            """
-            return succeed(True)
-
-        @patched
-        def removeInviteNotification(resourceSelf, record, request):
-            """
-            For testing purposes, removing an invite notification succeeds
-            without doing anything.
-            """
-            return succeed(True)
-
-        @patched
         def principalForCalendarUserAddress(resourceSelf, cuaddr):
             if "bogus" in cuaddr:
                 return None

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -51,7 +51,7 @@
 
 from txdav.base.propertystore.base import PropertyName
 from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
-    test_event_text, OTHER_HOME_UID
+    test_event_text
 from txdav.caldav.datastore.test.test_file import setUpCalendarStore
 from txdav.caldav.datastore.test.util import buildCalendarStore
 from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
@@ -59,7 +59,7 @@
 from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource
 from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
-    _BIND_STATUS_ACCEPTED, _BIND_MODE_WRITE, _BIND_STATUS_INVITED
+    _BIND_STATUS_ACCEPTED
 from txdav.common.datastore.test.util import populateCalendarsFrom, \
     CommonCommonTests
 from txdav.common.icommondatastore import NoSuchObjectResourceError
@@ -952,13 +952,13 @@
 
         @inlineCallbacks
         def _defer1():
-            yield cal1.shareWith(shareeHome=sharerHome1, mode=_BIND_MODE_DIRECT, status=_BIND_STATUS_ACCEPTED, summary="Shared Wiki Calendar")
+            yield cal1.directShareWithUser("uid2")
             yield txn1.commit()
         d1 = _defer1()
 
         @inlineCallbacks
         def _defer2():
-            yield cal2.shareWith(shareeHome=sharerHome2, mode=_BIND_MODE_DIRECT, status=_BIND_STATUS_ACCEPTED, summary="Shared Wiki Calendar")
+            yield cal2.directShareWithUser("uid1")
             yield txn2.commit()
         d2 = _defer2()
 
@@ -1708,90 +1708,6 @@
 
 
     @inlineCallbacks
-    def test_shareWithRevision(self):
-        """
-        Verify that bindRevision on calendars and shared calendars has the correct value.
-        """
-        cal = yield self.calendarUnderTest()
-        self.assertEqual(cal._bindRevision, 0)
-        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE)
-        yield self.commit()
-
-        normalCal = yield self.calendarUnderTest()
-        self.assertEqual(normalCal._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        otherCal = yield otherHome.childWithName(newCalName)
-        self.assertNotEqual(otherCal._bindRevision, 0)
-
-
-    @inlineCallbacks
-    def test_updateShareRevision(self):
-        """
-        Verify that bindRevision on calendars and shared calendars has the correct value.
-        """
-        cal = yield self.calendarUnderTest()
-        self.assertEqual(cal._bindRevision, 0)
-        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE, status=_BIND_STATUS_INVITED)
-        yield self.commit()
-
-        normalCal = yield self.calendarUnderTest()
-        self.assertEqual(normalCal._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        otherCal = yield otherHome.invitedObjectWithShareUID(newCalName)
-        self.assertEqual(otherCal._bindRevision, 0)
-        yield self.commit()
-
-        normalCal = yield self.calendarUnderTest()
-        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        otherCal = yield otherHome.invitedObjectWithShareUID(newCalName)
-        yield normalCal.updateShare(otherCal, status=_BIND_STATUS_ACCEPTED)
-        yield self.commit()
-
-        normalCal = yield self.calendarUnderTest()
-        self.assertEqual(normalCal._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        otherCal = yield otherHome.childWithName(newCalName)
-        self.assertNotEqual(otherCal._bindRevision, 0)
-
-
-    @inlineCallbacks
-    def test_sharedRevisions(self):
-        """
-        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
-        """
-        cal = yield self.calendarUnderTest()
-        self.assertEqual(cal._bindRevision, 0)
-        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE)
-        yield self.commit()
-
-        normalCal = yield self.calendarUnderTest()
-        self.assertEqual(normalCal._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        otherCal = yield otherHome.childWithName(newCalName)
-        self.assertNotEqual(otherCal._bindRevision, 0)
-
-        changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision - 1)
-        self.assertNotEqual(len(changed), 0)
-        self.assertEqual(len(deleted), 0)
-
-        changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision)
-        self.assertEqual(len(changed), 0)
-        self.assertEqual(len(deleted), 0)
-
-        for depth in ("1", "infinity",):
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision - 1, depth)
-            self.assertNotEqual(len(changed), 0)
-            self.assertEqual(len(deleted), 0)
-
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
-            self.assertEqual(len(changed), 0)
-            self.assertEqual(len(deleted), 0)
-
-
-    @inlineCallbacks
     def test_setAvailability(self):
         """
         Make sure a L{CalendarHome}.setAvailability() works.

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql_sharing.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/caldav/datastore/test/test_sql_sharing.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -26,14 +26,14 @@
     _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
 
 
-class CalendarSharing(CommonCommonTests, TestCase):
+class BaseSharingTests(CommonCommonTests, TestCase):
     """
-    Test twistedcaldav.scheduyling.implicit with a Request object.
+    Test store-based calendar sharing.
     """
 
     @inlineCallbacks
     def setUp(self):
-        yield super(CalendarSharing, self).setUp()
+        yield super(BaseSharingTests, self).setUp()
         self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
         yield self.populate()
 
@@ -43,12 +43,27 @@
         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": {
             },
@@ -95,6 +110,9 @@
         returnValue(sharedName)
 
 
+
+class CalendarSharing(BaseSharingTests):
+
     @inlineCallbacks
     def test_no_shares(self):
         """
@@ -104,6 +122,7 @@
         calendar = yield self.calendarUnderTest(home="user01", name="calendar")
         invites = yield calendar.sharingInvites()
         self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
 
 
     @inlineCallbacks
@@ -116,6 +135,7 @@
         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("user02", _BIND_MODE_READ, "summary")
         invites = yield calendar.sharingInvites()
@@ -123,7 +143,6 @@
         self.assertEqual(invites[0].uid, shareeView.shareUID())
         self.assertEqual(invites[0].ownerUID, "user01")
         self.assertEqual(invites[0].shareeUID, "user02")
-        self.assertEqual(invites[0].shareeName, shareeView.name())
         self.assertEqual(invites[0].mode, _BIND_MODE_READ)
         self.assertEqual(invites[0].status, _BIND_STATUS_INVITED)
         self.assertEqual(invites[0].summary, "summary")
@@ -137,6 +156,8 @@
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [inviteUID, ])
 
+        self.assertTrue(calendar.isShared())
+
         yield self.commit()
 
         # Uninvite
@@ -152,7 +173,16 @@
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(notifications, [])
 
+        self.assertTrue(calendar.isShared())
 
+        yield self.commit()
+
+        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):
         """
@@ -163,6 +193,7 @@
         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("user02", _BIND_MODE_READ, "summary")
         invites = yield calendar.sharingInvites()
@@ -177,6 +208,8 @@
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 1)
 
+        self.assertTrue(calendar.isShared())
+
         yield self.commit()
 
         # Accept
@@ -190,6 +223,9 @@
         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
@@ -203,17 +239,21 @@
         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+accept creates shares and notifications.
+        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("user02", _BIND_MODE_READ, "summary")
         invites = yield calendar.sharingInvites()
@@ -228,6 +268,8 @@
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 1)
 
+        self.assertTrue(calendar.isShared())
+
         yield self.commit()
 
         # Decline
@@ -241,6 +283,9 @@
         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
@@ -254,7 +299,10 @@
         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):
         """
@@ -266,6 +314,7 @@
         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("user02", _BIND_MODE_READ, "summary")
         invites = yield calendar.sharingInvites()
@@ -280,6 +329,8 @@
         notifications = yield notifyHome.listNotificationObjects()
         self.assertEqual(len(notifications), 1)
 
+        self.assertTrue(calendar.isShared())
+
         yield self.commit()
 
         # Accept
@@ -293,6 +344,9 @@
         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
@@ -306,7 +360,10 @@
         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):
         """
@@ -382,6 +439,7 @@
         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("user02")
         invites = yield calendar.sharingInvites()
@@ -389,7 +447,6 @@
         self.assertEqual(invites[0].uid, shareeView.shareUID())
         self.assertEqual(invites[0].ownerUID, "user01")
         self.assertEqual(invites[0].shareeUID, "user02")
-        self.assertEqual(invites[0].shareeName, shareeView.name())
         self.assertEqual(invites[0].mode, _BIND_MODE_DIRECT)
         self.assertEqual(invites[0].status, _BIND_STATUS_ACCEPTED)
 
@@ -431,3 +488,85 @@
         calendar = yield home.calendarWithName(shared_name)
         self.assertEquals(calendar.notifierID(), ("CalDAV", "user01/calendar",))
         yield self.commit()
+
+
+
+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)
+        otherCal = yield self.calendarUnderTest(home="user02", 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("user02", _BIND_MODE_READ, "summary")
+        newCalName = shareeView.shareUID()
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+        otherHome = yield self.homeUnderTest(name="user02")
+        otherCal = yield otherHome.anyObjectWithShareUID(newCalName)
+        self.assertEqual(otherCal._bindRevision, 0)
+        yield self.commit()
+
+        shareeHome = yield self.homeUnderTest(name="user02")
+        shareeView = yield shareeHome.acceptShare(newCalName)
+        sharedName = shareeView.name()
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest(home="user01", name="calendar")
+        self.assertEqual(normalCal._bindRevision, 0)
+        otherCal = yield self.calendarUnderTest(home="user02", 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)
+        otherHome = yield self.homeUnderTest(name="user02")
+        otherCal = yield self.calendarUnderTest(home="user02", name=sharedName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+        changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision - 1)
+        self.assertNotEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision)
+        self.assertEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        for depth in ("1", "infinity",):
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision - 1, depth)
+            self.assertNotEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/sql.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/sql.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # #
+from txdav.xml import element
 
 """
 SQL backend for CardDAV storage.
@@ -41,8 +42,8 @@
 from twistedcaldav import carddavxml, customxml
 from twistedcaldav.config import config
 from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError, \
-    vCardProductID, Property
+from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError, Property, \
+    vCardProductID
 
 from txdav.base.propertystore.base import PropertyName
 from txdav.base.propertystore.sql import PropertyStore
@@ -55,7 +56,8 @@
 from txdav.common.datastore.sql_tables import _ABO_KIND_PERSON, \
     _ABO_KIND_GROUP, _ABO_KIND_RESOURCE, _ABO_KIND_LOCATION, schema, \
     _BIND_MODE_OWN, _BIND_MODE_WRITE, _BIND_STATUS_ACCEPTED, \
-    _BIND_STATUS_DECLINED, _BIND_STATUS_INVITED, _BIND_MODE_READ
+    _BIND_STATUS_INVITED, _BIND_MODE_READ, _BIND_MODE_INDIRECT, \
+    _BIND_STATUS_DECLINED
 from txdav.common.icommondatastore import InternalDataStoreError, \
     InvalidUIDError, UIDExistsError, ObjectResourceTooBigError, \
     InvalidObjectResourceError, InvalidComponentForStoreError, \
@@ -141,8 +143,7 @@
 
         result = yield super(AddressBookHome, self).initFromStore(no_cache)
         if result is not None:
-
-            # created owned address book
+            # Created owned address book
             addressbook = AddressBook(
                 home=self,
                 name="addressbook",
@@ -151,6 +152,12 @@
                 status=_BIND_STATUS_ACCEPTED,
             )
             yield addressbook._loadPropertyStore()
+
+            # Extra check for shared
+            invites = yield addressbook.sharingInvites()
+            if len(invites) != 0:
+                addressbook._bindMessage = "shared"
+
             self._addressbook = addressbook
 
         returnValue(result)
@@ -194,6 +201,22 @@
 
 
     @inlineCallbacks
+    def anyObjectWithShareUID(self, shareUID):
+        """
+        Retrieve the child accepted or otherwise with the given bind identifier contained in this
+        home.
+
+        @param name: a string.
+        @return: an L{ICalendar} or C{None} if no such child exists.
+        """
+        result = yield super(AddressBookHome, self).anyObjectWithShareUID(shareUID)
+        if result is None:
+            result = yield AddressBookObject.objectWithBindName(self, shareUID, accepted=None)
+
+        returnValue(result)
+
+
+    @inlineCallbacks
     def removeUnacceptedShares(self):
         """
         Unbinds any collections that have been shared to this home but not yet
@@ -323,6 +346,31 @@
         return "addressbook"
 
 
+    @inlineCallbacks
+    def deleteShare(self):
+        """
+        This share is being deleted - decline and decline shared groups too.
+        """
+
+        ownerView = yield self.ownerView()
+        if self.direct():
+            yield ownerView.removeShare(self)
+        else:
+            if self.fullyShared():
+                yield self.declineShare()
+            else:
+                # Decline each shared group
+                acceptedGroupIDs = yield self.acceptedGroupIDs()
+                if acceptedGroupIDs:
+                    rows = (yield self._objectResourceNamesWithResourceIDsQuery(acceptedGroupIDs).on(
+                        self._txn, resourceIDs=acceptedGroupIDs
+                    ))
+                    groupNames = sorted([row[0] for row in rows])
+                    for group in groupNames:
+                        groupObject = yield self.objectResourceWithName(group)
+                        yield groupObject.declineShare()
+
+
     def newShareName(self):
         """
         For shared address books the resource name of a share is the ownerUID of the owner's resource.
@@ -330,158 +378,52 @@
         return self.ownerHome().uid()
 
 
-#    @inlineCallbacks
-#    def updateShare(self, shareeView, mode=None, status=None, summary=None):
-#        """
-#        Update share mode, status, and message for a home child shared with
-#        this (owned) L{CommonHomeChild}.
-#
-#        @param shareeView: The sharee home child that shares this.
-#        @type shareeView: L{CommonHomeChild}
-#
-#        @param mode: The sharing mode; L{_BIND_MODE_READ} or
-#            L{_BIND_MODE_WRITE} or None to not update
-#        @type mode: L{str}
-#
-#        @param status: The sharing status; L{_BIND_STATUS_INVITED} or
-#            L{_BIND_STATUS_ACCEPTED} or L{_BIND_STATUS_DECLINED} or
-#            L{_BIND_STATUS_INVALID}  or None to not update
-#        @type status: L{str}
-#
-#        @param summary: The proposed message to go along with the share, which
-#            will be used as the default display name, or None to not update
-#        @type summary: L{str}
-#
-#        @return: the name of the shared item in the sharee's home.
-#        @rtype: a L{Deferred} which fires with a L{str}
-#        """
-#        # TODO: raise a nice exception if shareeView is not, in fact, a shared
-#        # version of this same L{CommonHomeChild}
-#
-#        # remove None parameters, and substitute None for empty string
-#        bind = self._bindSchema
-#        columnMap = dict([(k, v if v != "" else None)
-#                          for k, v in {bind.BIND_MODE:mode,
-#                            bind.BIND_STATUS:status,
-#                            bind.MESSAGE:summary}.iteritems() if v is not None])
-#
-#        if len(columnMap):
-#
-#            # count accepted
-#            if status is not None:
-#                previouslyAcceptedBindCount = 1 if not shareeView.indirect() else 0
-#                previouslyAcceptedBindCount += len((yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
-#                        self._txn, homeID=shareeView.viewerHome()._resourceID, addressbookID=shareeView._resourceID
-#                )))
-#
-#            yield self._updateBindColumnsQuery(columnMap).on(
-#                self._txn,
-#                resourceID=self._resourceID, homeID=shareeView.viewerHome()._resourceID
-#            )
-#
-#            # update affected attributes
-#            if mode is not None:
-#                shareeView._bindMode = columnMap[bind.BIND_MODE]
-#
-#            if status is not None:
-#                shareeView._bindStatus = columnMap[bind.BIND_STATUS]
-#                if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
-#                    if 0 == previouslyAcceptedBindCount:
-#                        yield shareeView._initSyncToken()
-#                        yield shareeView._initBindRevision()
-#                        shareeView.viewerHome()._children[shareeView._name] = shareeView
-#                        shareeView.viewerHome()._children[shareeView._resourceID] = shareeView
-#                elif shareeView._bindStatus == _BIND_STATUS_DECLINED:
-#                    if 1 == previouslyAcceptedBindCount:
-#                        yield shareeView._deletedSyncToken(sharedRemoval=True)
-#                        shareeView.viewerHome()._children.pop(shareeView._name, None)
-#                        shareeView.viewerHome()._children.pop(shareeView._resourceID, None)
-#
-#            if summary is not None:
-#                shareeView._bindMessage = columnMap[bind.MESSAGE]
-#
-#            queryCacher = self._txn._queryCacher
-#            if queryCacher:
-#                cacheKey = queryCacher.keyForObjectWithName(shareeView._home._resourceID, shareeView._name)
-#                yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-#                cacheKey = queryCacher.keyForObjectWithResourceID(shareeView._home._resourceID, shareeView._resourceID)
-#                yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-#
-#            # Must send notification to ensure cache invalidation occurs
-#            yield self.notifyPropertyChanged()
+    @inlineCallbacks
+    def newShare(self, displayname=None):
+        """
+        Override in derived classes to do any specific operations needed when a share
+        is first accepted.
+        """
 
+        # For a direct share we will copy any displayname over using the owners view
+        if self.direct():
+            ownerView = yield self.ownerView()
+            try:
+                displayname = ownerView.properties()[PropertyName.fromElement(element.DisplayName)]
+                self.properties()[PropertyName.fromElement(element.DisplayName)] = displayname
+            except KeyError:
+                pass
 
-#    @inlineCallbacks
-#    def unshareWith(self, shareeHome):
-#        """
-#        Remove the shared version of this (owned) L{CommonHomeChild} from the
-#        referenced L{CommonHome}.
-#
-#        @see: L{CommonHomeChild.shareWith}
-#
-#        @param shareeHome: The home with which this L{CommonHomeChild} was
-#            previously shared.
-#
-#        @return: a L{Deferred} which will fire with the previous shareUID
-#        """
-#        sharedAddressBook = yield shareeHome.addressbookWithName(self.name())
-#        if sharedAddressBook:
-#
-#            acceptedBindCount = 1 if not sharedAddressBook.indirect() else 0
-#            acceptedBindCount += len((yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
-#                    self._txn, homeID=shareeHome._resourceID, addressbookID=sharedAddressBook._resourceID
-#            )))
-#            if acceptedBindCount == 1:
-#                yield sharedAddressBook._deletedSyncToken(sharedRemoval=True)
-#                shareeHome._children.pop(self.name(), None)
-#                shareeHome._children.pop(sharedAddressBook._resourceID, None)
-#            elif sharedAddressBook.indirect():
-#                # FIXME: remove objects for this group only using self.removeObjectResource
-#                self._objectNames = None
-#
-#            # Must send notification to ensure cache invalidation occurs
-#            yield self.notifyPropertyChanged()
-#
-#        # delete binds including invites
-#        deletedBindNameRows = yield self._deleteBindForResourceIDAndHomeID.on(self._txn, resourceID=self._resourceID,
-#             homeID=shareeHome._resourceID
-#        )
-#        if deletedBindNameRows:
-#            deletedBindName = deletedBindNameRows[0][0]
-#            queryCacher = self._txn._queryCacher
-#            if queryCacher:
-#                cacheKey = queryCacher.keyForObjectWithName(shareeHome._resourceID, self.name())
-#                queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-#        else:
-#            deletedBindName = None
-#
-#        self._initIsShared()
-#        returnValue(deletedBindName)
 
+    def fullyShared(self):
+        return not self.owned() and not self.indirect() and self.accepted()
 
-#    @inlineCallbacks
-#    def _isSharedOrInvited(self):
-#        """
-#        return True if this L{AddressBook} is shared or invited
-#        """
-#        sharedRows = []
-#        if self.owned():
-#            bind = self._bindSchema
-#            sharedRows = yield self._bindFor(
-#                (bind.RESOURCE_ID == Parameter("resourceID"))).on(
-#                self._txn, resourceID=self._resourceID,
-#            )
-#
-#        returnValue(bool(sharedRows))
-#
-#
-#    @inlineCallbacks
-#    def _initIsShared(self):
-#        isShared = yield self._isSharedOrInvited()
-#        self.setShared(isShared)
 
+    @inlineCallbacks
+    def _previousAcceptCount(self):
+        previouslyAcceptedBindCount = 1 if self.fullyShared() else 0
+        previouslyAcceptedBindCount += len((yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
+            self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
+        )))
+        returnValue(previouslyAcceptedBindCount)
 
 
+    @inlineCallbacks
+    def _changedStatus(self, previouslyAcceptedCount):
+        if self._bindStatus == _BIND_STATUS_ACCEPTED:
+            if 0 == previouslyAcceptedCount:
+                yield self._initSyncToken()
+                yield self._initBindRevision()
+                self._home._children[self._name] = self
+                self._home._children[self._resourceID] = self
+        elif self._bindStatus == _BIND_STATUS_DECLINED:
+            if 1 == previouslyAcceptedCount:
+                yield self._deletedSyncToken(sharedRemoval=True)
+                self._home._children.pop(self._name, None)
+                self._home._children.pop(self._resourceID, None)
+
+
+
 class AddressBook(AddressBookSharingMixIn, CommonHomeChild):
     """
     SQL-based implementation of L{IAddressBook}.
@@ -512,7 +454,8 @@
 
 
     def setCreated(self, newValue):
-        self.ownerHome()._created = newValue
+        if newValue is not None:
+            self.ownerHome()._created = newValue
 
 
     def getModified(self):
@@ -520,21 +463,13 @@
 
 
     def setModified(self, newValue):
-        self.ownerHome()._modified = newValue
+        if newValue is not None:
+            self.ownerHome()._modified = newValue
 
     _created = property(getCreated, setCreated,)
     _modified = property(getModified, setModified,)
 
-    ownerAddressBookHome = CommonHomeChild.ownerHome
-    viewerAddressBookHome = CommonHomeChild.viewerHome
-    addressbookObjects = CommonHomeChild.objectResources
-    listAddressBookObjects = CommonHomeChild.listObjectResources
-    addressbookObjectWithName = CommonHomeChild.objectResourceWithName
-    addressbookObjectWithUID = CommonHomeChild.objectResourceWithUID
-    createAddressBookObjectWithName = CommonHomeChild.createObjectResourceWithName
-    addressbookObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
 
-
     @inlineCallbacks
     def _loadPropertyStore(self, props=None):
         if props is None:
@@ -618,44 +553,44 @@
 
     @inlineCallbacks
     def listObjectResources(self):
-        # Check for non-group shared
-        if self.owned() or not self.indirect():
-            result = yield super(AddressBook, self).listObjectResources()
-            returnValue(result)
-
-        # Group shared
         if self._objectNames is None:
-            acceptedGroupIDs = yield self.acceptedGroupIDs()
-            allowedObjectIDs = yield self.expandGroupIDs(self._txn, acceptedGroupIDs)
-            rows = (yield self._objectResourceNamesWithResourceIDsQuery(allowedObjectIDs).on(
-                self._txn, resourceIDs=allowedObjectIDs
-            ))
-            objectNames = [row[0] for row in rows]
+            # Check for non-group shared
+            if self.owned() or self.fullyShared():
+                yield super(AddressBook, self).listObjectResources()
 
-            # account for fully-shared address book group
-            if not self.indirect():
-                if not self._groupForSharedAddressBookName() in objectNames:
-                    objectNames.append(self._groupForSharedAddressBookName())
-            self._objectNames = sorted(objectNames)
+            # Group shared
+            else:
+                acceptedGroupIDs = yield self.acceptedGroupIDs()
+                allowedObjectIDs = yield self.expandGroupIDs(self._txn, acceptedGroupIDs)
+                rows = (yield self._objectResourceNamesWithResourceIDsQuery(allowedObjectIDs).on(
+                    self._txn, resourceIDs=allowedObjectIDs
+                ))
+                self._objectNames = sorted([row[0] for row in rows])
 
+            # Account for fully-shared address book group
+            if self.fullyShared():
+                if not self._groupForSharedAddressBookName() in self._objectNames:
+                    self._objectNames.append(self._groupForSharedAddressBookName())
+
         returnValue(self._objectNames)
 
 
     @inlineCallbacks
     def countObjectResources(self):
-        # Check for non-group shared
-        if self.owned() or not self.indirect():
-            result = yield super(AddressBook, self).countObjectResources()
-            returnValue(result)
-
-        # Group shared
         if self._objectNames is None:
-            acceptedGroupIDs = yield self.acceptedGroupIDs()
-            count = len((yield self.expandGroupIDs(self._txn, acceptedGroupIDs)))
+            # Check for non-group shared
+            if self.owned() or self.fullyShared():
+                count = yield super(AddressBook, self).countObjectResources()
 
-            # account for fully-shared address book group
-            if not self.indirect():
+            # Group shared
+            else:
+                acceptedGroupIDs = yield self.acceptedGroupIDs()
+                count = len((yield self.expandGroupIDs(self._txn, acceptedGroupIDs)))
+
+            # Account for fully-shared address book group
+            if self.fullyShared():
                 count += 1
+
             returnValue(count)
 
         returnValue(len(self._objectNames))
@@ -696,7 +631,7 @@
     @inlineCallbacks
     def _groupForSharedAddressBookComponent(self):
 
-        n = self.name()
+        n = self.viewerHome().uid()
         fn = n
         uid = self._groupForSharedAddressBookUID()
 
@@ -708,14 +643,18 @@
         component = VCard.fromString(
             """BEGIN:VCARD
 VERSION:3.0
-PRODID:%s
-UID:%s
-FN:%s
-N:%s;;;;
+PRODID:{prodid}
+UID:{uid}
+FN:{fn}
+N:{n};;;;
 X-ADDRESSBOOKSERVER-KIND:group
 END:VCARD
-""".replace("\n", "\r\n") % (vCardProductID, uid, n, fn,)
-        )
+""".replace("\n", "\r\n").format(
+            prodid=vCardProductID,
+            uid=uid,
+            fn=fn,
+            n=n,
+        ))
 
         # then get member UIDs
         abo = schema.ADDRESSBOOK_OBJECT
@@ -754,39 +693,26 @@
 
         @return: an iterable of C{str}s.
         """
-        names = yield super(AddressBook, cls).listObjects(home)
-        names.insert(0, home.addressbook().name())
-        returnValue(names)
 
-#    @classmethod
-#    @inlineCallbacks
-#    def listObjects(cls, home):
-#        """
-#        Retrieve the names of the children with invitations in the given home.
-#
-#        @return: an iterable of C{str}s.
-#        """
-#        names = set([home.addressbook().name()])
-#
-#        rows = yield cls._acceptedBindForHomeID.on(
-#            home._txn, homeID=home._resourceID
-#        )
-#        for row in rows:
-#            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
-#            ownerHome = yield home._txn.homeWithResourceID(home._homeType, resourceID, create=True)
-#            names |= set([ownerHome.uid()])
-#
-#        groupRows = yield AddressBookObject._acceptedBindForHomeID.on(
-#            home._txn, homeID=home._resourceID
-#        )
-#        for groupRow in groupRows:
-#            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
-#            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
-#            ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID, create=True)
-#            names |= set([ownerHome.uid()])
-#        returnValue(tuple(names))
+        # Default address book
+        names = set([home.addressbook().name()])
 
+        # Fully shared address books
+        names |= set((yield super(AddressBook, cls).listObjects(home)))
 
+        # Group shared
+        groupRows = yield AddressBookObject._acceptedBindForHomeID.on(
+            home._txn, homeID=home._resourceID
+        )
+        for groupRow in groupRows:
+            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
+            ownerHome = yield home._txn.homeWithResourceID(home._homeType, ownerAddressBookID, create=True)
+            names |= set([ownerHome.uid()])
+
+        returnValue(tuple(names))
+
+
     @classmethod
     @inlineCallbacks
     def loadAllObjects(cls, home):
@@ -798,93 +724,78 @@
         optimization for Depth:1 operations on the home.
         """
 
-        results = yield super(AddressBook, cls).loadAllObjects(home)
-        results.insert(0, home.addressbook())
-        returnValue(results)
+        results = [home.addressbook()]
+        ownerHomeToDataRowMap = {}
 
+        # Load from the main table first
+        dataRows = yield cls._childrenAndMetadataForHomeID.on(
+            home._txn, homeID=home._resourceID
+        )
+        # get ownerHomeIDs
+        for dataRow in dataRows:
+            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
+            ownerHome = yield home.ownerHomeWithChildID(resourceID)
+            ownerHomeToDataRowMap[ownerHome] = dataRow
 
-#    @classmethod
-#    @inlineCallbacks
-#    def loadAllObjects(cls, home):
-#        """
-#        Load all L{CommonHomeChild} instances which are children of a given
-#        L{CommonHome} and return a L{Deferred} firing a list of them.  This must
-#        create the child classes and initialize them using "batched" SQL
-#        operations to keep this constant wrt the number of children.  This is an
-#        optimization for Depth:1 operations on the home.
-#        """
-#        results = [home.addressbook()]
-#        ownerHomeToDataRowMap = {}
-#
-#        # Load from the main table first
-#        dataRows = yield cls._childrenAndMetadataForHomeID.on(
-#            home._txn, homeID=home._resourceID
-#        )
-#        # get ownerHomeIDs
-#        for dataRow in dataRows:
-#            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
-#            ownerHome = yield home.ownerHomeWithChildID(resourceID)
-#            ownerHomeToDataRowMap[ownerHome] = dataRow
-#
-#        # now get group rows:
-#        groupBindRows = yield AddressBookObject._childrenAndMetadataForHomeID.on(
-#            home._txn, homeID=home._resourceID
-#        )
-#        for groupBindRow in groupBindRows:
-#            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
-#            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
-#            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
-#            if ownerHome not in ownerHomeToDataRowMap:
-#                groupBindRow[0] = _BIND_MODE_WRITE
-#                groupBindRow[3] = None  # name
-#                groupBindRow[4] = None  # bindStatus
-#                groupBindRow[6] = None  # bindMessage
-#                ownerHomeToDataRowMap[ownerHome] = groupBindRow
-#
-#        if ownerHomeToDataRowMap:
-#            # Get property stores for all these child resources (if any found)
-#            addressbookPropertyStoreIDs = [ownerHome._addressbookPropertyStoreID for ownerHome in ownerHomeToDataRowMap]
-#            propertyStores = yield PropertyStore.forMultipleResourcesWithResourceIDs(
-#                home.uid(), home._txn, addressbookPropertyStoreIDs
-#            )
-#
-#            addressbookResourceIDs = [ownerHome.addressbook()._resourceID for ownerHome in ownerHomeToDataRowMap]
-#            revisions = yield cls._revisionsForResourceIDs(addressbookResourceIDs).on(home._txn, resourceIDs=addressbookResourceIDs)
-#            revisions = dict(revisions)
-#
-#            # Create the actual objects merging in properties
-#            for ownerHome, dataRow in ownerHomeToDataRowMap.iteritems():
-#                bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
-#                additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
-#                metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
-#
-#                child = cls(
-#                    home=home,
-#                    name=name,
-#                    resourceID=ownerHome._resourceID,
-#                    mode=bindMode,
-#                    status=bindStatus,
-#                    revision=bindRevision,
-#                    message=bindMessage,
-#                    ownerHome=ownerHome,
-#                )
-#
-#                for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
-#                    setattr(child, attr, value)
-#                for attr, value in zip(cls.metadataAttributes(), metadata):
-#                    setattr(child, attr, value)
-#                child._syncTokenRevision = revisions[child._resourceID]
-#                propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
-#                # We have to re-adjust the property store object to account for possible shared
-#                # collections as previously we loaded them all as if they were owned
-#                if propstore:
-#                    propstore._setDefaultUserUID(ownerHome.uid())
-#                yield child._loadPropertyStore(propstore)
-#                results.append(child)
-#
-#        returnValue(results)
+        # now get group rows:
+        groupBindRows = yield AddressBookObject._childrenAndMetadataForHomeID.on(
+            home._txn, homeID=home._resourceID
+        )
+        for groupBindRow in groupBindRows:
+            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
+            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
+            if ownerHome not in ownerHomeToDataRowMap:
+                groupBindRow[0] = _BIND_MODE_INDIRECT
+                groupBindRow[3] = None  # bindName
+                groupBindRow[4] = None  # bindStatus
+                groupBindRow[6] = None  # bindMessage
+                ownerHomeToDataRowMap[ownerHome] = groupBindRow
 
+        if ownerHomeToDataRowMap:
+            # Get property stores for all these child resources (if any found)
+            addressbookPropertyStoreIDs = [ownerHome._addressbookPropertyStoreID for ownerHome in ownerHomeToDataRowMap]
+            propertyStores = yield PropertyStore.forMultipleResourcesWithResourceIDs(
+                home.uid(), home._txn, addressbookPropertyStoreIDs
+            )
 
+            addressbookResourceIDs = [ownerHome.addressbook()._resourceID for ownerHome in ownerHomeToDataRowMap]
+            revisions = yield cls._revisionsForResourceIDs(addressbookResourceIDs).on(home._txn, resourceIDs=addressbookResourceIDs)
+            revisions = dict(revisions)
+
+            # Create the actual objects merging in properties
+            for ownerHome, dataRow in ownerHomeToDataRowMap.iteritems():
+                bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
+                additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+                metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+
+                child = cls(
+                    home=home,
+                    name=ownerHome.uid(),
+                    resourceID=ownerHome._resourceID,
+                    mode=bindMode,
+                    status=bindStatus,
+                    revision=bindRevision,
+                    message=bindMessage,
+                    ownerHome=ownerHome,
+                )
+
+                for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
+                    setattr(child, attr, value)
+                for attr, value in zip(cls.metadataAttributes(), metadata):
+                    setattr(child, attr, value)
+                child._syncTokenRevision = revisions[child._resourceID]
+                propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
+                # We have to re-adjust the property store object to account for possible shared
+                # collections as previously we loaded them all as if they were owned
+                if propstore:
+                    propstore._setDefaultUserUID(ownerHome.uid())
+                yield child._loadPropertyStore(propstore)
+                results.append(child)
+
+        returnValue(results)
+
+
     @classmethod
     @inlineCallbacks
     def objectWithName(cls, home, name, accepted=True):
@@ -899,94 +810,18 @@
         @return: an L{CommonHomeChild} or C{None} if no such child exists.
         """
 
+        # Try owned address book first
         if name == home.addressbook().name():
             returnValue(home.addressbook())
 
+        # Try fully shared next
         result = yield super(AddressBook, cls).objectWithName(home, name, accepted)
-        returnValue(result)
 
+        # Look for indirect shares
+        if result is None:
+            result = yield cls._indirectObjectWithNameOrID(home, name=name, accepted=accepted)
 
-#    @classmethod
-#    @inlineCallbacks
-#    def objectWithName(cls, home, name, accepted=True):
-#        """
-#        Retrieve the child with the given C{name} contained in the given
-#        C{home}.
-#
-#        @param home: a L{CommonHome}.
-#
-#        @param name: a string; the name of the L{CommonHomeChild} to retrieve.
-#
-#        @return: an L{CommonHomeChild} or C{None} if no such child
-#            exists.
-#        """
-#        if accepted and name == home.addressbook().name():
-#            returnValue(home.addressbook())
-#        # shared address books only from this point on
-#
-#        rows = None
-#        queryCacher = home._txn._queryCacher
-#        ownerHome = None
-#
-#        if queryCacher:
-#            # Retrieve data from cache
-#            cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
-#            rows = yield queryCacher.get(cacheKey)
-#
-#        if not rows:
-#            # name must be a home uid
-#            ownerHome = yield home._txn.addressbookHomeWithUID(name)
-#            if ownerHome:
-#                # see if address book resource id in bind table
-#                ownerAddressBook = ownerHome.addressbook()
-#                bindRows = yield cls._bindForResourceIDAndHomeID.on(
-#                    home._txn, resourceID=ownerAddressBook._resourceID, homeID=home._resourceID
-#                )
-#                if bindRows:
-#                    bindRows[0].insert(cls.bindColumnCount, ownerAddressBook._resourceID)
-#                    bindRows[0].insert(cls.bindColumnCount + 1, bindRows[0][4])  # cachedStatus = bindStatus
-#                    rows = bindRows
-#                else:
-#                    groupBindRows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
-#                            home._txn, homeID=home._resourceID, addressbookID=ownerAddressBook._resourceID
-#                    )
-#                    for groupBindRow in groupBindRows:
-#                        groupBindRow.insert(AddressBookObject.bindColumnCount, ownerAddressBook._resourceID)
-#                        groupBindRow.insert(AddressBookObject.bindColumnCount + 1, groupBindRow[4])
-#                        groupBindRow[0] = _BIND_MODE_WRITE
-#                        groupBindRow[3] = ownerHome.uid()  # bindName
-#                        groupBindRow[4] = None  # bindStatus
-#                        groupBindRow[6] = None  # bindMessage
-#                        rows = [groupBindRow]
-#                        break
-#
-#            if rows and queryCacher:
-#                # Cache the result
-#                queryCacher.setAfterCommit(home._txn, cacheKey, rows)
-#
-#        if not rows:
-#            returnValue(None)
-#
-#        row = rows[0]
-#        bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage, ownerAddressBookID, cachedBindStatus = row[:cls.bindColumnCount + 2] #@UnusedVariable
-#
-#        # if wrong status, exit here.  Item is in queryCache
-#        if accepted is not None and (cachedBindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
-#            returnValue(None)
-#
-#        ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
-#        child = cls(
-#                home=home,
-#                name=name,
-#                resourceID=ownerAddressBookID,
-#                mode=bindMode,
-#                status=bindStatus,
-#                revision=bindRevision,
-#                message=bindMessage,
-#                ownerHome=ownerHome,
-#            )
-#        yield child.initFromStore()
-#        returnValue(child)
+        returnValue(result)
 
 
     @classmethod
@@ -1001,64 +836,88 @@
         @return: an L{CommonHomeChild} or C{None} if no such child
             exists.
         """
+
+        # Try owned address book first
         if home._resourceID == resourceID:
             returnValue(home.addressbook())
 
+        # Try fully shared next
         result = yield super(AddressBook, cls).objectWithID(home, resourceID, accepted)
+
+        # Look for indirect shares
+        if result is None:
+            result = yield cls._indirectObjectWithNameOrID(home, resourceID=resourceID, accepted=accepted)
+
         returnValue(result)
 
 
-#    @classmethod
-#    @inlineCallbacks
-#    def objectWithID(cls, home, resourceID, accepted=True):
-#        """
-#        Retrieve the child with the given C{resourceID} contained in the given
-#        C{home}.
-#
-#        @param home: a L{CommonHome}.
-#        @param resourceID: a string.
-#        @return: an L{CommonHomeChild} or C{None} if no such child
-#            exists.
-#        """
-#        if home._resourceID == resourceID:
-#            returnValue(home.addressbook())
-#
-#        bindRows = yield cls._bindForResourceIDAndHomeID.on(
-#            home._txn, resourceID=resourceID, homeID=home._resourceID
-#        )
-#        if bindRows:
-#            bindRow = bindRows[0]
-#            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = bindRow[:cls.bindColumnCount] #@UnusedVariable
-#
-#            if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
-#                returnValue(None)
-#
-#            ownerHome = yield home.ownerHomeWithChildID(resourceID)
-#            if bindStatus == _BIND_STATUS_ACCEPTED:
-#                returnValue((yield home.childWithName(ownerHome.uid())))
-#            else:
-#                returnValue((yield cls.objectWithName(home, ownerHome.uid(), accepted=False)))
-#
-#        groupBindRows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
-#                    home._txn, homeID=home._resourceID, addressbookID=resourceID
-#        )
-#        if groupBindRows:
-#            groupBindRow = groupBindRows[0]
-#            bindMode, homeID, resourceID, name, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
-#
-#            if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
-#                returnValue(None)
-#
-#            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, resourceID)
-#            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
-#            if bindStatus == _BIND_STATUS_ACCEPTED:
-#                returnValue((yield home.childWithName(ownerHome.uid())))
-#            else:
-#                returnValue((yield cls.objectWithName(home, ownerHome.uid(), accepted=False)))
-#
-#        returnValue(None)
+    @classmethod
+    @inlineCallbacks
+    def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
+        # replaces objectWithName()
+        """
+        Synthesize and indirect child for matching name or id based on whether shared groups exist.
 
+        @param home: a L{CommonHome}.
 
+        @param name: a string; the name of the L{CommonHomeChild} to retrieve.
+
+        @return: an L{CommonHomeChild} or C{None} if no such child
+            exists.
+        """
+        rows = None
+        ownerHome = None
+
+        # TODO: add queryCacher support
+
+        if rows is None:
+            # No cached copy
+            if name:
+                ownerHome = yield home._txn.addressbookHomeWithUID(name)
+                if ownerHome is None:
+                    returnValue(None)
+                resourceID = ownerHome.addressbook()._resourceID
+            rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
+                home._txn, homeID=home._resourceID, addressbookID=resourceID
+            )
+
+        if not rows:
+            returnValue(None)
+
+        groupID = None
+        overallBindStatus = _BIND_STATUS_INVITED
+        minBindRevision = None
+        for row in rows:
+            bindMode, homeID, resourceGroupID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
+            if groupID is None:
+                groupID = resourceGroupID
+            minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
+            if bindStatus == _BIND_STATUS_ACCEPTED:
+                overallBindStatus = _BIND_STATUS_ACCEPTED
+
+        if accepted is not None and (overallBindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
+            returnValue(None)
+        additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+
+        if ownerHome is None:
+            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
+            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
+
+        child = cls(
+            home=home,
+            name=ownerHome.uid(),
+            resourceID=resourceID,
+            mode=_BIND_MODE_INDIRECT,
+            status=overallBindStatus,
+            revision=minBindRevision,
+            message="",
+            ownerHome=ownerHome,
+            ownerName=ownerHome.uid()
+        )
+        yield child.initFromStore(additionalBind)
+        returnValue(child)
+
+
     @classmethod
     def _memberIDsWithGroupIDsQuery(cls, groupIDs):
         """
@@ -1181,15 +1040,15 @@
         ids = set([row[1] for row in rows])
 
         # Everything is read-write
-        if self.owned() or self._bindMode == _BIND_MODE_WRITE:
+        if self.owned() or self.fullyShared() and self._bindMode == _BIND_MODE_WRITE:
             returnValue(tuple(readOnlyIDs), tuple(ids))
 
         # Fully shared but mode is read-only
-        if self._bindMode == _BIND_MODE_READ:
+        if self.fullyShared() and self._bindMode == _BIND_MODE_READ:
             ids |= set([self._resourceID, ])
             readOnlyIDs = set(ids)
 
-        # Look for shared groups and for those that tare read-write, transfer their object ids
+        # Look for shared groups and for those that are read-write, transfer their object ids
         # to the read-write set
         groupBindRows = yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
             self._txn, homeID=self._home._resourceID, addressbookID=self._resourceID
@@ -1229,36 +1088,333 @@
         readOnlyIDs, readWriteIDs = yield self.accessControlObjectIDs()
         returnValue((readOnlyIDs + readWriteIDs))
 
+    # Convenient names for some methods
+    ownerAddressBookHome = CommonHomeChild.ownerHome
+    viewerAddressBookHome = CommonHomeChild.viewerHome
+    addressbookObjects = CommonHomeChild.objectResources
+    listAddressBookObjects = listObjectResources
+    countAddressBookObjects = countObjectResources
+    addressbookObjectWithName = CommonHomeChild.objectResourceWithName
+    addressbookObjectWithUID = CommonHomeChild.objectResourceWithUID
+    createAddressBookObjectWithName = CommonHomeChild.createObjectResourceWithName
+    addressbookObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
 
 
+
 class AddressBookObjectSharingMixIn(SharingMixIn):
     """
-        Sharing code for AddressBookObject
+    Sharing code for AddressBookObject
     """
 
-#    @inlineCallbacks
-#    def _isSharedOrInvited(self):
-#        """
-#        return True if this L{AddressBook} is shared or invited
-#        """
-#        sharedRows = []
-#        if self.owned():
-#            bind = self._bindSchema
-#            sharedRows = yield self._bindFor(
-#                (bind.RESOURCE_ID == Parameter("resourceID"))).on(
-#                self._txn, resourceID=self._resourceID,
-#            )
-#
-#        returnValue(bool(sharedRows))
-#
-#
-#    @inlineCallbacks
-#    def _initIsShared(self):
-#        isShared = yield self._isSharedOrInvited()
-#        self.setShared(isShared)
+    def sharedResourceType(self):
+        """
+        The sharing resource type
+        """
+        return "group"
 
 
+    #
+    # Lower level API
+    #
 
+    @inlineCallbacks
+    def ownerView(self):
+        """
+        Return the owner resource counterpart of this shared resource.
+        """
+        # Get the child of the owner home that has the same resource id as the owned one
+        ownerView = yield self.ownerHome().addressbook().objectResourceWithID(self.id())
+        returnValue(ownerView)
+
+
+    @inlineCallbacks
+    def shareeView(self, shareeUID):
+        """
+        Return the shared resource counterpart of this owned resource for the specified sharee.
+        """
+
+        # Get the shared address book, then the child within
+        shareeAdbk = yield self.addressbook().shareeView(shareeUID)
+        shareeView = (yield shareeAdbk.objectResourceWithID(self.id())) if shareeAdbk is not None else None
+        returnValue(shareeView if shareeView is not None and shareeView.shareMode() is not None else None)
+
+
+    @inlineCallbacks
+    def shareWith(self, shareeHome, mode, status=None, summary=None):
+        """
+        Share this (owned) L{AddressBookObject} with another home.
+
+        @param shareeHome: The home of the sharee.
+        @type shareeHome: L{CommonHome}
+
+        @param mode: The sharing mode; L{_BIND_MODE_READ} or
+            L{_BIND_MODE_WRITE} or L{_BIND_MODE_DIRECT}
+        @type mode: L{str}
+
+        @param status: The sharing status; L{_BIND_STATUS_INVITED} or
+            L{_BIND_STATUS_ACCEPTED}
+        @type mode: L{str}
+
+        @param summary: The proposed message to go along with the share, which
+            will be used as the default display name.
+        @type summary: L{str}
+
+        @return: the name of the shared group in the sharee home.
+        @rtype: L{str}
+        """
+
+        if status is None:
+            status = _BIND_STATUS_ACCEPTED
+
+        @inlineCallbacks
+        def doInsert(subt):
+            newName = self.newShareName()
+            yield self._bindInsertQuery.on(
+                subt,
+                homeID=shareeHome._resourceID,
+                resourceID=self._resourceID,
+                name=newName,
+                mode=mode,
+                bindStatus=status,
+                message=summary
+            )
+            returnValue(newName)
+        try:
+            bindName = yield self._txn.subtransaction(doInsert)
+        except AllRetriesFailed:
+            group = yield self.shareeView(shareeHome.uid())
+            yield self.updateShare(
+                group, mode=mode, status=status,
+                summary=summary
+            )
+            bindName = group.name()
+        else:
+            if status == _BIND_STATUS_ACCEPTED:
+                shareeView = yield shareeHome.anyObjectWithShareUID(bindName)
+                yield shareeView.addressbook()._initSyncToken()
+                yield shareeView._initBindRevision()
+
+        queryCacher = self._txn._queryCacher
+        if queryCacher:
+            cacheKey = queryCacher.keyForObjectWithName(shareeHome._resourceID, self.addressbook().name())
+            queryCacher.invalidateAfterCommit(self._txn, cacheKey)
+
+        yield self.setShared(True)
+
+        # Must send notification to ensure cache invalidation occurs
+        yield self.notifyChanged()
+
+        returnValue(bindName)
+
+
+    @inlineCallbacks
+    def createShare(self, shareeUID, mode, summary=None):
+        """
+        Create a new shared resource. If the mode is direct, the share is created in accepted state,
+        otherwise the share is created in invited state.
+        """
+
+        if self._kind == _ABO_KIND_GROUP:
+            shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary)
+            returnValue(shareeView)
+        else:
+            returnValue(None)
+
+
+    @inlineCallbacks
+    def unshare(self):
+        """
+        Unshares a group, regardless of which "direction" it was shared.
+        """
+        if self._kind == _ABO_KIND_GROUP:
+            yield super(AddressBookObjectSharingMixIn, self).unshare()
+
+
+    @inlineCallbacks
+    def updateShare(self, shareeView, mode=None, status=None, summary=None):
+        """
+        Update share mode, status, and message for a home child shared with
+        this (owned) L{CommonHomeChild}.
+
+        @param shareeView: The sharee home child that shares this.
+        @type shareeView: L{CommonHomeChild}
+
+        @param mode: The sharing mode; L{_BIND_MODE_READ} or
+            L{_BIND_MODE_WRITE} or None to not update
+        @type mode: L{str}
+
+        @param status: The sharing status; L{_BIND_STATUS_INVITED} or
+            L{_BIND_STATUS_ACCEPTED} or L{_BIND_STATUS_DECLINED} or
+            L{_BIND_STATUS_INVALID}  or None to not update
+        @type status: L{str}
+
+        @param summary: The proposed message to go along with the share, which
+            will be used as the default display name, or None to not update
+        @type summary: L{str}
+
+        @return: the name of the shared item in the sharee's home.
+        @rtype: a L{Deferred} which fires with a L{str}
+        """
+        # TODO: raise a nice exception if shareeView is not, in fact, a shared
+        # version of this same L{CommonHomeChild}
+
+        # remove None parameters, and substitute None for empty string
+        bind = self._bindSchema
+        columnMap = dict([(k, v if v != "" else None) for k, v in {
+            bind.BIND_MODE:mode,
+            bind.BIND_STATUS:status,
+            bind.MESSAGE:summary
+        }.iteritems() if v is not None])
+
+        if len(columnMap):
+
+            # count accepted
+            if status is not None:
+                previouslyAcceptedBindCount = 1 if self.addressbook().fullyShared() else 0
+                previouslyAcceptedBindCount += len((
+                    yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
+                        self._txn, homeID=shareeView.viewerHome()._resourceID, addressbookID=self.addressbook()._resourceID
+                    )
+                ))
+
+            yield self._updateBindColumnsQuery(columnMap).on(
+                self._txn,
+                resourceID=self._resourceID,
+                homeID=shareeView.addressbook().viewerHome()._resourceID
+            )
+
+            # update affected attributes
+            if mode is not None:
+                shareeView._bindMode = columnMap[bind.BIND_MODE]
+
+            if status is not None:
+                shareeView._bindStatus = columnMap[bind.BIND_STATUS]
+                if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
+                    if 0 == previouslyAcceptedBindCount:
+                        yield shareeView.addressbook()._initSyncToken()
+                        yield shareeView._initBindRevision()
+                        shareeView.viewerHome()._children[shareeView.addressbook().name()] = shareeView.addressbook()
+                        shareeView.viewerHome()._children[shareeView.addressbook().id()] = shareeView.addressbook()
+                elif shareeView._bindStatus != _BIND_STATUS_INVITED:
+                    if 1 == previouslyAcceptedBindCount:
+                        yield shareeView.addressbook()._deletedSyncToken(sharedRemoval=True)
+                        shareeView.viewerHome()._children.pop(shareeView.addressbook().name(), None)
+                        shareeView.viewerHome()._children.pop(shareeView.addressbook().id(), None)
+
+            if summary is not None:
+                shareeView._bindMessage = columnMap[bind.MESSAGE]
+
+            # Must send notification to ensure cache invalidation occurs
+            yield self.notifyChanged()
+
+
+    @inlineCallbacks
+    def removeShare(self, shareeView):
+        """
+        Remove the shared version of this (owned) L{CommonHomeChild} from the
+        referenced L{CommonHome}.
+
+        @see: L{CommonHomeChild.shareWith}
+
+        @param shareeHome: The home with which this L{CommonHomeChild} was
+            previously shared.
+
+        @return: a L{Deferred} which will fire with the previously-used name.
+        """
+
+        shareeHome = shareeView.addressbook().viewerHome()
+        sharedAddressBook = yield shareeHome.addressbookWithName(self.addressbook().name())
+
+        if sharedAddressBook:
+
+            acceptedBindCount = 1 if sharedAddressBook.fullyShared() else 0
+            acceptedBindCount += len((
+                yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
+                    self._txn, homeID=shareeHome._resourceID, addressbookID=sharedAddressBook._resourceID
+                )
+            ))
+
+            if acceptedBindCount == 1:
+                yield sharedAddressBook._deletedSyncToken(sharedRemoval=True)
+                shareeHome._children.pop(shareeView.addressbook().name(), None)
+                shareeHome._children.pop(shareeView.addressbook().id(), None)
+
+            # Must send notification to ensure cache invalidation occurs
+            yield self.notifyChanged()
+
+        # Must send notification to ensure cache invalidation occurs
+        yield self.notifyChanged()
+
+        # delete binds including invites
+        yield self._deleteBindForResourceIDAndHomeID.on(
+            self._txn,
+            resourceID=self._resourceID,
+            homeID=shareeHome._resourceID,
+        )
+
+        queryCacher = self._txn._queryCacher
+        if queryCacher:
+            cacheKey = queryCacher.keyForObjectWithName(shareeHome._resourceID, self.addressbook().name())
+            queryCacher.invalidateAfterCommit(self._txn, cacheKey)
+
+
+    @inlineCallbacks
+    def sharingInvites(self):
+        """
+        Retrieve the list of all L{SharingInvitation} for this L{CommonHomeChild}, irrespective of mode.
+
+        @return: L{SharingInvitation} objects
+        @rtype: a L{Deferred} which fires with a L{list} of L{SharingInvitation}s.
+        """
+        if not self.owned() or self._kind != _ABO_KIND_GROUP:
+            returnValue([])
+
+        # Get all binds
+        acceptedRows = yield self._sharedInvitationBindForResourceID.on(
+            self._txn, resourceID=self._resourceID, homeID=self.addressbook()._home._resourceID
+        )
+
+        result = []
+        for homeUID, homeRID, resourceID, resourceName, bindMode, bindStatus, bindMessage in acceptedRows: #@UnusedVariable
+            invite = SharingInvitation(
+                resourceName,
+                self.addressbook()._home.name(),
+                self.addressbook()._home.id(),
+                homeUID,
+                homeRID,
+                bindMode,
+                bindStatus,
+                bindMessage,
+            )
+            result.append(invite)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def _initBindRevision(self):
+        # FIXME: not sure about all this revision stuff
+        yield self.addressbook()._initBindRevision()
+
+        bind = self._bindSchema
+        yield self._updateBindColumnsQuery(
+            {bind.BIND_REVISION : Parameter("revision"), }
+        ).on(
+            self._txn,
+            revision=self.addressbook()._bindRevision,
+            resourceID=self._resourceID,
+            homeID=self.viewerHome()._resourceID,
+        )
+        yield self.invalidateQueryCache()
+
+
+    def shareUID(self):
+        """
+        @see: L{ICalendar.shareUID}
+        """
+        return self._bindName if self._bindName else self._name
+
+
+
 class AddressBookObject(CommonObjectResource, AddressBookObjectSharingMixIn):
 
     implements(IAddressBookObject)
@@ -1325,10 +1481,10 @@
             yield self.unshare() # storebridge should already have done this
         else:
             # handled in storebridge as unshare, should not be here.  assert instead?
-            if self.isGroupForSharedAddressBook() or self.shareUID():
+            if self.isGroupForSharedAddressBook() or self._bindMode is not None:
                 raise HTTPError(FORBIDDEN)
 
-        if not self.owned() and self.addressbook().indirect():
+        if not self.owned() and not self.addressbook().fullyShared():
             readWriteObjectIDs = []
             readWriteGroupIDs = yield self.addressbook().readWriteGroupIDs()
             if readWriteGroupIDs:
@@ -1372,17 +1528,16 @@
     @inlineCallbacks
     def readWriteAccess(self):
         assert not self.owned(), "Don't call items in owned address book"
-        yield None
 
-        #shared address book group is always read-only
+        # Shared address book group always read-only
         if self.isGroupForSharedAddressBook():
             returnValue(False)
 
-        # if fully shared and rw, must be RW since sharing group read-only has no affect
-        if not self.addressbook().indirect() and self.addressbook().shareMode() == _BIND_MODE_WRITE:
+        # If fully shared and rw, must be RW since sharing group read-only has no affect
+        if self.addressbook().fullyShared() and self.addressbook().shareMode() == _BIND_MODE_WRITE:
             returnValue(True)
 
-        #otherwise, must be in a read-write group
+        # Otherwise, must be in a read-write group
         readWriteGroupIDs = yield self.addressbook().readWriteGroupIDs()
         readWriteIDs = yield self.addressbook().expandGroupIDs(self._txn, readWriteGroupIDs)
         returnValue(self._resourceID in readWriteIDs)
@@ -1420,7 +1575,39 @@
             Where=obj.RESOURCE_ID == Parameter("resourceID"),)
 
 
+    @classmethod
     @inlineCallbacks
+    def objectWithBindName(cls, home, name, accepted):
+        """
+        Retrieve the objectResource with the given bind name C{name} contained in the given
+        C{home}.
+
+        @param home: a L{CommonHome}.
+
+        @param name: a string; the name of the L{CommonHomeChild} to retrieve.
+
+        @return: an L{ObjectResource} or C{None} if no such resource exists.
+        """
+
+        groupBindRows = yield cls._bindForNameAndHomeID.on(
+            home._txn, name=name, homeID=home._resourceID
+        )
+        if groupBindRows:
+            groupBindRow = groupBindRows[0]
+            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
+
+            if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
+                returnValue(None)
+
+            ownerAddressBookID = yield cls.ownerAddressBookIDFromGroupID(home._txn, resourceID)
+            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
+            addressbook = yield home.anyObjectWithShareUID(ownerHome.uid())
+            returnValue((yield addressbook.objectResourceWithID(resourceID)))
+
+        returnValue(None)
+
+
+    @inlineCallbacks
     def initFromStore(self):
         """
         Initialise this object from the store. We read in and cache all the
@@ -1431,11 +1618,31 @@
         @return: L{self} if object exists in the DB, else C{None}
         """
         abo = None
-        if self.owned() or not self.addressbook().indirect():  # owned or fully shared
+        if self.owned() or self.addressbook().fullyShared():  # owned or fully shared
             abo = yield super(AddressBookObject, self).initFromStore()
+
+            # Might be special group
+            if abo is None and self.addressbook().fullyShared():
+                rows = None
+                if self._name:
+                    if self._name == self.addressbook()._groupForSharedAddressBookName():
+                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
+                elif self._uid:
+                    if self._uid == (yield self.addressbook()._groupForSharedAddressBookUID()):
+                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
+                elif self._resourceID:
+                    if self.isGroupForSharedAddressBook():
+                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
+
+                if rows:
+                    self._initFromRow(tuple(rows[0]))
+                    yield self._loadPropertyStore()
+                    abo = self
+
         else:
             acceptedGroupIDs = yield self.addressbook().acceptedGroupIDs()
             allowedObjectIDs = yield self.addressbook().expandGroupIDs(self._txn, acceptedGroupIDs)
+            rows = None
             if self._name:
                 if allowedObjectIDs:
                     rows = (yield self._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
@@ -1475,9 +1682,11 @@
                     self._bindStatus = bindStatus
                     self._bindMessage = bindMessage
                     self._bindName = bindName
+                else:
+                    invites = yield self.sharingInvites()
+                    if len(invites):
+                        self._bindMessage = "shared"
 
-                #yield self._initIsShared()
-
             returnValue(self)
         else:
             returnValue(None)
@@ -1532,8 +1741,10 @@
     @classmethod
     @inlineCallbacks
     def _allColumnsWithParent(cls, addressbook):
-        if addressbook.owned() or not addressbook.indirect():
+        if addressbook.owned() or addressbook.fullyShared():
             rows = yield super(AddressBookObject, cls)._allColumnsWithParent(addressbook)
+            if addressbook.fullyShared():
+                rows.append(addressbook._groupForSharedAddressBookRow())
         else:
             acceptedGroupIDs = yield addressbook.acceptedGroupIDs()
             allowedObjectIDs = yield addressbook.expandGroupIDs(addressbook._txn, acceptedGroupIDs)
@@ -1555,8 +1766,10 @@
     @inlineCallbacks
     def _allColumnsWithParentAndNames(cls, addressbook, names):
 
-        if addressbook.owned() or not addressbook.indirect():
+        if addressbook.owned() or addressbook.fullyShared():
             rows = yield super(AddressBookObject, cls)._allColumnsWithParentAndNames(addressbook, names)
+            if addressbook.fullyShared() and addressbook._groupForSharedAddressBookName() in names:
+                rows.append(addressbook._groupForSharedAddressBookRow())
         else:
             acceptedGroupIDs = yield addressbook.acceptedGroupIDs()
             allowedObjectIDs = yield addressbook.expandGroupIDs(addressbook._txn, acceptedGroupIDs)
@@ -1649,7 +1862,7 @@
                 raise InvalidUIDError("Cannot change the UID in an existing resource.")
         else:
             # for partially shared addressbooks, cannot use name that already exists in owner
-            if not self.owned() and self.addressbook().indirect():
+            if not self.owned() and not self.addressbook().fullyShared():
                 nameElsewhere = (yield self.ownerHome().addressbook().addressbookObjectWithName(self.name()))
                 if nameElsewhere is not None:
                     raise ObjectResourceNameAlreadyExistsError(self.name() + ' in use by owning addressbook.')
@@ -1680,7 +1893,7 @@
             if self._kind == _ABO_KIND_GROUP:  # optimization
                 invites = yield self.sharingInvites()
                 for invite in invites:
-                    shareeHome = (yield self._txn.homeWithResourceID(self.addressbook()._home._homeType, invite.shareeHomeID()))
+                    shareeHome = (yield self._txn.homeWithResourceID(self.addressbook()._home._homeType, invite.shareeHomeID))
                     yield self._changeAddressBookRevision(shareeHome.addressbook(), inserting)
                     # one is enough because all have the same resourceID
                     break
@@ -1784,7 +1997,7 @@
             foundUIDs.append(self._uid) # circular self reference is OK
             missingUIDs = set(memberUIDs) - set(foundUIDs)
 
-            if not self.owned() and self.addressbook().indirect():
+            if not self.owned() and not self.addressbook().fullyShared():
                 # in partially shared addressbook, all members UIDs must be inside the shared groups
                 # except during bulk operations, when other UIDs added are OK
                 coaddedUIDs = set() if self._options.get("coaddedUIDs") is None else self._options["coaddedUIDs"]
@@ -1851,7 +2064,7 @@
             ).on(self._txn)
             groupIDs = set([groupIDRow[0] for groupIDRow in groupIDRows])
 
-            if not self.owned() and self.addressbook().indirect():
+            if not self.owned() and not self.addressbook().fullyShared():
                 readWriteGroupIDs = yield self.addressbook().readWriteGroupIDs()
                 assert readWriteGroupIDs, "no access"
                 groupIDs |= set(readWriteGroupIDs)
@@ -2017,13 +2230,6 @@
         pass
 
 
-    def sharedResourceType(self):
-        """
-        The sharing resource type
-        """
-        return "group"
-
-
     # IDataStoreObject
     def contentType(self):
         """
@@ -2032,18 +2238,6 @@
         return MimeType.fromString("text/vcard; charset=utf-8")
 
 
-    def owned(self):
-        return self.addressbook().owned()
-
-
-    def ownerHome(self):
-        return self.addressbook().ownerHome()
-
-
-    def viewerHome(self):
-        return self.addressbook().viewerHome()
-
-
     @classmethod
     def metadataColumns(cls):
         """
@@ -2076,6 +2270,13 @@
         return self.addressbook().notifyChanged()
 
 
+    def notifyPropertyChanged(self):
+        """
+        Send notifications when properties on this object change.
+        """
+        return self.addressbook().notifyPropertyChanged()
+
+
     @classproperty
     def _addressbookIDForResourceID(cls): #@NoSelf
         obj = cls._objectSchema
@@ -2092,281 +2293,6 @@
         returnValue(ownerAddressBookIDRows[0][0])
 
 
-    @inlineCallbacks
-    def sharingInvites(self):
-        """
-        Retrieve the list of all L{SharingInvitation} for this L{CommonHomeChild}, irrespective of mode.
-
-        @return: L{SharingInvitation} objects
-        @rtype: a L{Deferred} which fires with a L{list} of L{SharingInvitation}s.
-        """
-        if not self.owned():
-            returnValue([])
-
-        # get all accepted binds
-        acceptedRows = yield self._sharedInvitationBindForResourceID.on(
-            self._txn, resourceID=self._resourceID, homeID=self.addressbook()._home._resourceID
-        )
-
-        result = []
-        for homeUID, homeRID, resourceID, resourceName, bindMode, bindStatus, bindMessage in acceptedRows: #@UnusedVariable
-            invite = SharingInvitation(
-                resourceName,
-                self.addressbook()._home.name(),
-                self.addressbook()._home._resourceID,
-                homeUID,
-                homeRID,
-                resourceID,
-                self.addressbook().name(),
-                bindMode,
-                bindStatus,
-                bindMessage,
-            )
-            result.append(invite)
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def unshare(self):
-        """
-        Unshares a group, regardless of which "direction" it was shared.
-        """
-        if self._kind == _ABO_KIND_GROUP:
-            if self.owned():
-                # This collection may be shared to others
-                invites = yield self.sharingInvites()
-                for invite in invites:
-                    shareeHome = (yield self._txn.homeWithResourceID(self.addressbook()._home._homeType, invite.shareeHomeID()))
-                    (yield self.unshareWith(shareeHome))
-            else:
-                # This collection is shared to me
-                ownerAddressBook = self.addressbook().ownerHome().addressbook()
-                ownerGroup = yield ownerAddressBook.objectResourceWithID(self._resourceID)
-                if ownerGroup:
-                    yield ownerGroup.unshareWith(self._home)
-
-
-    @inlineCallbacks
-    def unshareWith(self, shareeHome):
-        """
-        Remove the shared version of this (owned) L{CommonHomeChild} from the
-        referenced L{CommonHome}.
-
-        @see: L{CommonHomeChild.shareWith}
-
-        @param shareeHome: The home with which this L{CommonHomeChild} was
-            previously shared.
-
-        @return: a L{Deferred} which will fire with the previously-used name.
-        """
-        sharedAddressBook = yield shareeHome.addressbookWithName(self.addressbook().name())
-
-        if sharedAddressBook:
-
-            acceptedBindCount = 1 if not sharedAddressBook.indirect() else 0
-            acceptedBindCount += len((
-                yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
-                    self._txn, homeID=shareeHome._resourceID, addressbookID=sharedAddressBook._resourceID
-                )
-            ))
-
-            if acceptedBindCount == 1:
-                yield sharedAddressBook._deletedSyncToken(sharedRemoval=True)
-                shareeHome._children.pop(self.addressbook().name(), None)
-                shareeHome._children.pop(self.addressbook()._resourceID, None)
-
-            # Must send notification to ensure cache invalidation occurs
-            yield self.notifyChanged()
-
-        # delete binds including invites
-        deletedBindNameRows = yield self._deleteBindForResourceIDAndHomeID.on(
-            self._txn, resourceID=self._resourceID,
-             homeID=shareeHome._resourceID
-        )
-        if deletedBindNameRows:
-            deletedBindName = deletedBindNameRows[0][0]
-            queryCacher = self._txn._queryCacher
-            if queryCacher:
-                cacheKey = queryCacher.keyForObjectWithName(shareeHome._resourceID, self.addressbook().name())
-                queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-        else:
-            deletedBindName = None
-
-        #yield self._initIsShared()
-        returnValue(deletedBindName)
-
-
-    @inlineCallbacks
-    def shareWith(self, shareeHome, mode, status=None, summary=None):
-        """
-        Share this (owned) L{AddressBookObject} with another home.
-
-        @param shareeHome: The home of the sharee.
-        @type shareeHome: L{CommonHome}
-
-        @param mode: The sharing mode; L{_BIND_MODE_READ} or
-            L{_BIND_MODE_WRITE} or L{_BIND_MODE_DIRECT}
-        @type mode: L{str}
-
-        @param status: The sharing status; L{_BIND_STATUS_INVITED} or
-            L{_BIND_STATUS_ACCEPTED}
-        @type mode: L{str}
-
-        @param summary: The proposed message to go along with the share, which
-            will be used as the default display name.
-        @type summary: L{str}
-
-        @return: the name of the shared group in the sharee home.
-        @rtype: L{str}
-        """
-
-        if status is None:
-            status = _BIND_STATUS_ACCEPTED
-
-        @inlineCallbacks
-        def doInsert(subt):
-            newName = self.newShareName()
-            yield self._bindInsertQuery.on(
-                subt, homeID=shareeHome._resourceID,
-                resourceID=self._resourceID, name=newName,
-                mode=mode, bindStatus=status, message=summary
-            )
-            returnValue(newName)
-        try:
-            bindName = yield self._txn.subtransaction(doInsert)
-        except AllRetriesFailed:
-            # FIXME: catch more specific exception
-            groupBindRows = yield self._bindForResourceIDAndHomeID.on(
-                self._txn, resourceID=self._resourceID, homeID=shareeHome._resourceID
-            )
-            groupBindRow = groupBindRows[0]
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:self.bindColumnCount] #@UnusedVariable
-            if bindStatus == _BIND_STATUS_ACCEPTED:
-                group = yield shareeHome.objectWithShareUID(bindName)
-            else:
-                group = yield shareeHome.invitedObjectWithShareUID(bindName)
-            bindName = yield self.updateShare(
-                group, mode=mode, status=status,
-                summary=summary
-            )
-        else:
-            if status == _BIND_STATUS_ACCEPTED:
-                shareeView = yield shareeHome.objectWithShareUID(bindName)
-                yield shareeView.addressbook()._initSyncToken()
-                yield shareeView._initBindRevision()
-
-        queryCacher = self._txn._queryCacher
-        if queryCacher:
-            cacheKey = queryCacher.keyForObjectWithName(shareeHome._resourceID, self.addressbook().name())
-            queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-
-        # Must send notification to ensure cache invalidation occurs
-        yield self.notifyChanged()
-        self.setShared(True)
-        returnValue(bindName)
-
-
-    @inlineCallbacks
-    def _initBindRevision(self):
-        yield self.addressbook()._initBindRevision()
-
-        bind = self._bindSchema
-        yield self._updateBindColumnsQuery(
-            {bind.BIND_REVISION : Parameter("revision"), }).on(
-            self._txn,
-            revision=self.addressbook()._bindRevision,
-            resourceID=self._resourceID,
-            homeID=self.viewerHome()._resourceID,
-        )
-        yield self.invalidateQueryCache()
-
-
-    @inlineCallbacks
-    def updateShare(self, shareeView, mode=None, status=None, summary=None):
-        """
-        Update share mode, status, and message for a home child shared with
-        this (owned) L{CommonHomeChild}.
-
-        @param shareeView: The sharee home child that shares this.
-        @type shareeView: L{CommonHomeChild}
-
-        @param mode: The sharing mode; L{_BIND_MODE_READ} or
-            L{_BIND_MODE_WRITE} or None to not update
-        @type mode: L{str}
-
-        @param status: The sharing status; L{_BIND_STATUS_INVITED} or
-            L{_BIND_STATUS_ACCEPTED} or L{_BIND_STATUS_DECLINED} or
-            L{_BIND_STATUS_INVALID}  or None to not update
-        @type status: L{str}
-
-        @param summary: The proposed message to go along with the share, which
-            will be used as the default display name, or None to not update
-        @type summary: L{str}
-
-        @return: the name of the shared item in the sharee's home.
-        @rtype: a L{Deferred} which fires with a L{str}
-        """
-        # TODO: raise a nice exception if shareeView is not, in fact, a shared
-        # version of this same L{CommonHomeChild}
-
-        # remove None parameters, and substitute None for empty string
-        bind = self._bindSchema
-        columnMap = dict([(k, v if v != "" else None)
-                          for k, v in {bind.BIND_MODE:mode,
-                            bind.BIND_STATUS:status,
-                            bind.MESSAGE:summary}.iteritems() if v is not None])
-
-        if len(columnMap):
-
-            # count accepted
-            if status is not None:
-                previouslyAcceptedBindCount = 1 if not self.addressbook().indirect() else 0
-                previouslyAcceptedBindCount += len((
-                    yield AddressBookObject._acceptedBindForHomeIDAndAddressBookID.on(
-                        self._txn, homeID=shareeView.viewerHome()._resourceID, addressbookID=self.addressbook()._resourceID
-                    )
-                ))
-
-            bindNameRows = yield self._updateBindColumnsQuery(columnMap).on(
-                self._txn,
-                resourceID=self._resourceID, homeID=shareeView.viewerHome()._resourceID
-            )
-
-            # update affected attributes
-            if mode is not None:
-                shareeView._bindMode = columnMap[bind.BIND_MODE]
-
-            if status is not None:
-                shareeView._bindStatus = columnMap[bind.BIND_STATUS]
-                if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
-                    if 0 == previouslyAcceptedBindCount:
-                        yield shareeView.addressbook()._initSyncToken()
-                        yield shareeView._initBindRevision()
-                        shareeView.viewerHome()._children[self.addressbook().name()] = shareeView.addressbook()
-                        shareeView.viewerHome()._children[shareeView._resourceID] = shareeView.addressbook()
-                elif shareeView._bindStatus != _BIND_STATUS_INVITED:
-                    if 1 == previouslyAcceptedBindCount:
-                        yield shareeView.addressbook()._deletedSyncToken(sharedRemoval=True)
-                        shareeView.viewerHome()._children.pop(self.addressbook().name(), None)
-                        shareeView.viewerHome()._children.pop(shareeView._resourceID, None)
-
-            if summary is not None:
-                shareeView._bindMessage = columnMap[bind.MESSAGE]
-
-            # safer to just invalidate in all cases rather than calculate when to invalidate
-            queryCacher = self._txn._queryCacher
-            if queryCacher:
-                cacheKey = queryCacher.keyForObjectWithName(shareeView.viewerHome()._resourceID, self.addressbook().name())
-                queryCacher.invalidateAfterCommit(self._txn, cacheKey)
-
-            shareeView._name = bindNameRows[0][0]
-
-            # Must send notification to ensure cache invalidation occurs
-            yield self.notifyChanged()
-
-        returnValue(shareeView._name)
-
-
     @classproperty
     def _acceptedBindForHomeIDAndAddressBookID(cls): #@NoSelf
         bind = cls._bindSchema

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -22,7 +22,7 @@
 from twext.enterprise.dal.syntax import Select, Parameter
 
 from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, DeferredList
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.task import deferLater
 
 from twisted.trial import unittest
@@ -40,9 +40,7 @@
 
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from txdav.common.datastore.sql import EADDRESSBOOKTYPE, CommonObjectResource
-from txdav.common.datastore.sql_tables import  _ABO_KIND_PERSON, _ABO_KIND_GROUP, \
-    schema, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED, _BIND_MODE_WRITE, \
-    _BIND_STATUS_INVITED
+from txdav.common.datastore.sql_tables import  _ABO_KIND_PERSON, _ABO_KIND_GROUP, schema
 from txdav.common.datastore.test.util import buildStore
 
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
@@ -741,13 +739,13 @@
 
         @inlineCallbacks
         def _defer1():
-            yield ab1.shareWith(shareeHome=sharerHome1, mode=_BIND_MODE_DIRECT, status=_BIND_STATUS_ACCEPTED, summary="Shared Wiki AddressBook")
+            yield ab1.directShareWithUser("uid2")
             yield txn1.commit()
         d1 = _defer1()
 
         @inlineCallbacks
         def _defer2():
-            yield ab2.shareWith(shareeHome=sharerHome2, mode=_BIND_MODE_DIRECT, status=_BIND_STATUS_ACCEPTED, summary="Shared Wiki AddressBook")
+            yield ab2.directShareWithUser("uid1")
             yield txn2.commit()
         d2 = _defer2()
 
@@ -865,250 +863,3 @@
         obj = (yield self.addressbookObjectUnderTest())
         addressbookObject = (yield home.objectResourceWithID(obj._resourceID))
         self.assertNotEquals(addressbookObject, None)
-
-
-    @inlineCallbacks
-    def test_shareWithRevision(self):
-        """
-        Verify that bindRevision on addressbooks and shared addressbooks has the correct value.
-        """
-        ab = yield self.addressbookUnderTest()
-        self.assertEqual(ab._bindRevision, 0)
-        other = yield self.homeUnderTest(name="home2")
-        newABShareUID = yield ab.shareWith(other, _BIND_MODE_WRITE)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest()
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherAB = yield otherHome.objectWithShareUID(newABShareUID)
-        self.assertNotEqual(otherAB._bindRevision, 0)
-
-
-    @inlineCallbacks
-    def test_shareGroupWithRevision(self):
-        """
-        Verify that bindRevision on addressbooks and shared groups has the correct value.
-        """
-        ab = yield self.addressbookUnderTest(home="home3")
-        self.assertEqual(ab._bindRevision, 0)
-        group = yield ab.objectResourceWithName("4.vcf")
-        other = yield self.homeUnderTest(name="home2")
-        newGroupShareUID = yield group.shareWith(other, _BIND_MODE_WRITE)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest(home="home3")
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherGroup = yield otherHome.objectWithShareUID(newGroupShareUID)
-        otherAB = otherGroup.addressbook()
-        self.assertNotEqual(otherAB._bindRevision, 0)
-
-
-    @inlineCallbacks
-    def test_updateShareRevision(self):
-        """
-        Verify that bindRevision on addressbooks and shared addressbooks has the correct value.
-        """
-        ab = yield self.addressbookUnderTest()
-        self.assertEqual(ab._bindRevision, 0)
-        other = yield self.homeUnderTest(name="home2")
-        newABShareUID = yield ab.shareWith(other, _BIND_MODE_WRITE, status=_BIND_STATUS_INVITED)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest()
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherAB = yield otherHome.invitedObjectWithShareUID(newABShareUID)
-        self.assertEqual(otherAB._bindRevision, 0)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest()
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherAB = yield otherHome.invitedObjectWithShareUID(newABShareUID)
-        yield normalAB.updateShare(otherAB, status=_BIND_STATUS_ACCEPTED)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest()
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherAB = yield otherHome.objectWithShareUID(newABShareUID)
-        self.assertNotEqual(otherAB._bindRevision, 0)
-
-
-    @inlineCallbacks
-    def test_updateSharedGroupRevision(self):
-        """
-        Verify that bindRevision on addressbooks and shared addressbooks has the correct value.
-        """
-        ab = yield self.addressbookUnderTest(home="home3")
-        self.assertEqual(ab._bindRevision, 0)
-        group = yield ab.objectResourceWithName("4.vcf")
-        other = yield self.homeUnderTest(name="home2")
-        newGroupShareUID = yield group.shareWith(other, _BIND_MODE_WRITE, status=_BIND_STATUS_INVITED)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest(home="home3")
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherGroup = yield otherHome.invitedObjectWithShareUID(newGroupShareUID)
-        otherAB = otherGroup.addressbook()
-        self.assertEqual(otherAB._bindRevision, 0)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest(home="home3")
-        normalGroup = yield normalAB.objectResourceWithName("4.vcf")
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherGroup = yield otherHome.invitedObjectWithShareUID(newGroupShareUID)
-        yield normalGroup.updateShare(otherGroup, status=_BIND_STATUS_ACCEPTED)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest(home="home3")
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherGroup = yield otherHome.objectWithShareUID(newGroupShareUID)
-        otherAB = otherGroup.addressbook()
-        self.assertNotEqual(otherAB._bindRevision, 0)
-
-
-    @inlineCallbacks
-    def test_sharedRevisions(self):
-        """
-        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
-        """
-        ab = yield self.addressbookUnderTest()
-        self.assertEqual(ab._bindRevision, 0)
-        other = yield self.homeUnderTest(name="home2")
-        newABShareUID = yield ab.shareWith(other, _BIND_MODE_WRITE)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest()
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherAB = yield otherHome.objectWithShareUID(newABShareUID)
-        self.assertNotEqual(otherAB._bindRevision, 0)
-
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision - 1)
-        self.assertNotEqual(len(changed), 0)
-        self.assertEqual(len(deleted), 0)
-
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
-        self.assertEqual(len(changed), 0)
-        self.assertEqual(len(deleted), 0)
-
-        for depth in ("1", "infinity",):
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
-            self.assertNotEqual(len(changed), 0)
-            self.assertEqual(len(deleted), 0)
-
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
-            self.assertEqual(len(changed), 0)
-            self.assertEqual(len(deleted), 0)
-
-
-    @inlineCallbacks
-    def test_sharedGroupRevisions(self):
-        """
-        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
-        """
-        ab = yield self.addressbookUnderTest(home="home3")
-        self.assertEqual(ab._bindRevision, 0)
-        group = yield ab.objectResourceWithName("4.vcf")
-        other = yield self.homeUnderTest(name="home2")
-        newGroupShareUID = yield group.shareWith(other, _BIND_MODE_WRITE)
-        yield self.commit()
-
-        normalAB = yield self.addressbookUnderTest(home="home3")
-        self.assertEqual(normalAB._bindRevision, 0)
-        otherHome = yield self.homeUnderTest(name="home2")
-        otherGroup = yield otherHome.objectWithShareUID(newGroupShareUID)
-        otherAB = otherGroup.addressbook()
-        self.assertNotEqual(otherAB._bindRevision, 0)
-
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision - 1)
-        self.assertNotEqual(len(changed), 0)
-        self.assertEqual(len(deleted), 0)
-
-        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
-        self.assertEqual(len(changed), 0)
-        self.assertEqual(len(deleted), 0)
-
-        for depth in ("1", "infinity",):
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
-            self.assertNotEqual(len(changed), 0)
-            self.assertEqual(len(deleted), 0)
-
-            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
-            self.assertEqual(len(changed), 0)
-            self.assertEqual(len(deleted), 0)
-
-
-    @inlineCallbacks
-    def test_addressbookRevisionChangeConcurrency(self):
-        """
-        Test that two concurrent attempts to add resources in two separate
-        calendar homes does not deadlock on the revision table update.
-        """
-
-        # Make sure homes are provisioned
-        txn = self.transactionUnderTest()
-        home_uid1 = yield txn.homeWithUID(EADDRESSBOOKTYPE, "user01", create=True)
-        home_uid2 = yield txn.homeWithUID(EADDRESSBOOKTYPE, "user02", create=True)
-        self.assertNotEqual(home_uid1, None)
-        self.assertNotEqual(home_uid2, None)
-        yield self.commit()
-
-        # Create first events in different calendar homes
-        txn1 = self._sqlStore.newTransaction()
-        txn2 = self._sqlStore.newTransaction()
-
-        addressbook_uid1_in_txn1 = yield self.addressbookUnderTest(txn1, "addressbook", "user01")
-        addressbook_uid2_in_txn2 = yield self.addressbookUnderTest(txn2, "addressbook", "user02")
-
-        data = """BEGIN:VCARD
-VERSION:3.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-UID:data%(ctr)s
-FN:Data %(ctr)s
-N:Sub Group;;;;
-REV:20120503T194243Z
-END:VCARD
-
-"""
-
-        component = VComponent.fromString(data % {"ctr": 1})
-        yield addressbook_uid1_in_txn1.createAddressBookObjectWithName("data1.ics", component)
-
-        component = VComponent.fromString(data % {"ctr": 2})
-        yield addressbook_uid2_in_txn2.createAddressBookObjectWithName("data2.ics", component)
-
-        # Setup deferreds to run concurrently and create second events in the calendar homes
-        # previously used by the other transaction - this could create the deadlock.
-        @inlineCallbacks
-        def _defer_uid3():
-            addressbook_uid1_in_txn2 = yield self.addressbookUnderTest(txn2, "addressbook", "user01")
-            component = VComponent.fromString(data % {"ctr": 3})
-            yield addressbook_uid1_in_txn2.createAddressBookObjectWithName("data3.ics", component)
-            yield txn2.commit()
-        d1 = _defer_uid3()
-
-        @inlineCallbacks
-        def _defer_uid4():
-            addressbook_uid2_in_txn1 = yield self.addressbookUnderTest(txn1, "addressbook", "user02")
-            component = VComponent.fromString(data % {"ctr": 4})
-            yield addressbook_uid2_in_txn1.createAddressBookObjectWithName("data4.ics", component)
-            yield txn1.commit()
-        d2 = _defer_uid4()
-
-        # Now do the concurrent provision attempt
-        yield DeferredList([d1, d2])
-
-        # Verify we did not have a deadlock and all resources have been created.
-        vcarddata1 = yield self.addressbookObjectUnderTest(name="data1.ics", addressbook_name="addressbook", home="user01")
-        vcarddata2 = yield self.addressbookObjectUnderTest(name="data2.ics", addressbook_name="addressbook", home="user02")
-        vcarddata3 = yield self.addressbookObjectUnderTest(name="data3.ics", addressbook_name="addressbook", home="user01")
-        vcarddata4 = yield self.addressbookObjectUnderTest(name="data4.ics", addressbook_name="addressbook", home="user02")
-        self.assertNotEqual(vcarddata1, None)
-        self.assertNotEqual(vcarddata2, None)
-        self.assertNotEqual(vcarddata3, None)
-        self.assertNotEqual(vcarddata4, None)

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql_sharing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql_sharing.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/carddav/datastore/test/test_sql_sharing.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -15,24 +15,26 @@
 ##
 
 
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, DeferredList
 from twisted.trial.unittest import TestCase
 
-from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.vcard import Component as VCard, Component
 from twext.python.clsprop import classproperty
 from txdav.common.datastore.test.util import CommonCommonTests, buildStore
 from txdav.common.datastore.sql_tables import _BIND_MODE_READ, \
-    _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
+    _BIND_STATUS_INVITED, _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED, \
+    _BIND_MODE_WRITE
 
 
-class CalendarSharing(CommonCommonTests, TestCase):
+
+class BaseSharingTests(CommonCommonTests, TestCase):
     """
-    Test twistedcaldav.scheduyling.implicit with a Request object.
+    Test store-based address book sharing.
     """
 
     @inlineCallbacks
     def setUp(self):
-        yield super(CalendarSharing, self).setUp()
+        yield super(BaseSharingTests, self).setUp()
         self._sqlStore = yield buildStore(self, self.notifierFactory)
         yield self.populate()
 
@@ -57,12 +59,65 @@
         yield populateTxn.commit()
         self.notifierFactory.reset()
 
+    # Data to populate
+    card1 = """BEGIN:VCARD
+VERSION:3.0
+UID:card1
+FN:Card 1
+N:card1;;;;
+END:VCARD
+"""
 
+    card2 = """BEGIN:VCARD
+VERSION:3.0
+UID:card2
+FN:Card 2
+N:card2;;;;
+END:VCARD
+"""
+
+    card3 = """BEGIN:VCARD
+VERSION:3.0
+UID:card3
+FN:Card 3
+N:card3;;;;
+END:VCARD
+"""
+
+    group1 = """BEGIN:VCARD
+VERSION:3.0
+UID:group1
+FN:Group 1
+N:group1;;;;
+X-ADDRESSBOOKSERVER-KIND:group
+X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:card1
+X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:card2
+END:VCARD
+"""
+
+    group2 = """BEGIN:VCARD
+VERSION:3.0
+UID:group2
+FN:Group 2
+N:group2;;;;
+X-ADDRESSBOOKSERVER-KIND:group
+X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:card1
+X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:card3
+X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:foreign
+END:VCARD
+"""
+
+
     @classproperty(cache=False)
     def requirements(cls): #@NoSelf
         return {
         "user01": {
             "addressbook": {
+                "card1.vcf": cls.card1,
+                "card2.vcf": cls.card2,
+                "card3.vcf": cls.card3,
+                "group1.vcf": cls.group1,
+                "group2.vcf": cls.group2,
             },
         },
         "user02": {
@@ -75,7 +130,12 @@
         },
     }
 
+    fully_shared_children = ["addressbook.vcf", "group1.vcf", "group2.vcf", "card1.vcf", "card2.vcf", "card3.vcf", ]
+    all_children = ["group1.vcf", "group2.vcf", "card1.vcf", "card2.vcf", "card3.vcf", ]
+    group1_children = ["group1.vcf", "card1.vcf", "card2.vcf", ]
+    group2_children = ["group2.vcf", "card1.vcf", "card3.vcf", ]
 
+
     def storeUnderTest(self):
         """
         Create and return a L{CalendarStore} for testing.
@@ -84,16 +144,28 @@
 
 
     @inlineCallbacks
-    def _createShare(self):
+    def _createShare(self, mode=_BIND_MODE_READ):
+        inviteUID = yield self._inviteShare(mode)
+        sharedName = yield self._acceptShare(inviteUID)
+        returnValue(sharedName)
+
+
+    @inlineCallbacks
+    def _inviteShare(self, mode=_BIND_MODE_READ):
         # Invite
         addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 0)
 
-        shareeView = yield addressbook.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
+        shareeView = yield addressbook.inviteUserToShare("user02", mode, "summary")
         inviteUID = shareeView.shareUID()
         yield self.commit()
 
+        returnValue(inviteUID)
+
+
+    @inlineCallbacks
+    def _acceptShare(self, inviteUID):
         # Accept
         shareeHome = yield self.addressbookHomeUnderTest(name="user02")
         shareeView = yield shareeHome.acceptShare(inviteUID)
@@ -104,6 +176,78 @@
 
 
     @inlineCallbacks
+    def _createGroupShare(self, groupname="group1.vcf", mode=_BIND_MODE_READ):
+        inviteUID = yield self._inviteGroupShare(groupname, mode)
+        sharedName = yield self._acceptGroupShare(inviteUID)
+        returnValue(sharedName)
+
+
+    @inlineCallbacks
+    def _inviteGroupShare(self, groupname="group1.vcf", mode=_BIND_MODE_READ):
+        # Invite
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name=groupname)
+        shareeView = yield group.inviteUserToShare("user02", mode, "summary")
+        inviteUID = shareeView.shareUID()
+        yield self.commit()
+
+        returnValue(inviteUID)
+
+
+    @inlineCallbacks
+    def _acceptGroupShare(self, inviteUID):
+        # Accept
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.acceptShare(inviteUID)
+        yield self.commit()
+
+        returnValue(inviteUID)
+
+
+    @inlineCallbacks
+    def _check_notifications(self, home, items):
+        notifyHome = yield self.transactionUnderTest().notificationsWithUID(home)
+        notifications = yield notifyHome.listNotificationObjects()
+        self.assertEqual(set(notifications), set(items))
+
+
+    @inlineCallbacks
+    def _check_addressbook(self, home, addressbook_name, child_names):
+        sharedParent = yield self.addressbookUnderTest(home=home, name=addressbook_name)
+        self.assertTrue(sharedParent is not None, msg="Missing parent:{}".format(addressbook_name))
+
+        children = yield sharedParent.listAddressBookObjects()
+        self.assertEqual(set(children), set(child_names))
+
+        number = yield sharedParent.countAddressBookObjects()
+        self.assertEqual(number, len(child_names))
+
+        for child in child_names:
+            shared = yield self.addressbookObjectUnderTest(home=home, addressbook_name=addressbook_name, name=child)
+            self.assertTrue(shared is not None, msg="Missing child:{}".format(child))
+
+
+    @inlineCallbacks
+    def _check_read_only(self, home, addressbook_name, child_names):
+        for child in child_names:
+            shared = yield self.addressbookObjectUnderTest(home=home, addressbook_name=addressbook_name, name=child)
+            rw_mode = yield shared.readWriteAccess()
+            self.assertFalse(rw_mode)
+
+
+    @inlineCallbacks
+    def _check_read_write(self, home, addressbook_name, child_names):
+        for child in child_names:
+            shared = yield self.addressbookObjectUnderTest(home=home, addressbook_name=addressbook_name, name=child)
+            rw_mode = yield shared.readWriteAccess()
+            self.assertTrue(rw_mode, msg="Wrong mode: {}".format(child))
+
+
+
+class AddressBookSharing(BaseSharingTests):
+    """
+    Test store-based address book sharing.
+    """
+    @inlineCallbacks
     def test_no_shares(self):
         """
         Test that initially there are no shares.
@@ -112,6 +256,7 @@
         addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 0)
+        self.assertFalse(addressbook.isShared())
 
 
     @inlineCallbacks
@@ -124,6 +269,7 @@
         addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 0)
+        self.assertFalse(addressbook.isShared())
 
         shareeView = yield addressbook.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
         invites = yield addressbook.sharingInvites()
@@ -131,38 +277,41 @@
         self.assertEqual(invites[0].uid, shareeView.shareUID())
         self.assertEqual(invites[0].ownerUID, "user01")
         self.assertEqual(invites[0].shareeUID, "user02")
-        self.assertEqual(invites[0].shareeName, shareeView.name())
         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()
 
-        self.assertEqual(invites[0].shareeName, "user01")
-
         sharedName = shareeView.name()
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID, ])
+        yield self._check_notifications("user02", [inviteUID, ])
 
+        self.assertTrue(addressbook.isShared())
+
         yield self.commit()
 
         # Uninvite
         addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 1)
+        self.assertTrue(addressbook.isShared())
 
         yield addressbook.uninviteUserFromShare("user02")
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 0)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [])
+        yield self._check_notifications("user02", [])
 
+        self.assertTrue(addressbook.isShared())
 
+        yield self.commit()
+
+        addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertFalse(addressbook.isShared())
+
+
     @inlineCallbacks
     def test_accept_share(self):
         """
@@ -173,6 +322,7 @@
         addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 0)
+        self.assertFalse(addressbook.isShared())
 
         shareeView = yield addressbook.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
         invites = yield addressbook.sharingInvites()
@@ -183,10 +333,10 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(len(notifications), 1)
+        yield self._check_notifications("user02", [inviteUID, ])
 
+        self.assertTrue(addressbook.isShared())
+
         yield self.commit()
 
         # Accept
@@ -196,10 +346,12 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is not None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
+        addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertTrue(addressbook.isShared())
+
         yield self.commit()
 
         # Re-accept
@@ -209,21 +361,24 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is not None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
+        addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertTrue(addressbook.isShared())
 
+
     @inlineCallbacks
     def test_decline_share(self):
         """
-        Test that invite+accept creates shares and notifications.
+        Test that invite+decline does not create shares but does create notifications.
         """
 
         # Invite
         addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 0)
+        self.assertFalse(addressbook.isShared())
 
         shareeView = yield addressbook.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
         invites = yield addressbook.sharingInvites()
@@ -234,10 +389,10 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(len(notifications), 1)
+        yield self._check_notifications("user02", [inviteUID, ])
 
+        self.assertTrue(addressbook.isShared())
+
         yield self.commit()
 
         # Decline
@@ -247,10 +402,11 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
+        addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertTrue(addressbook.isShared())
+
         yield self.commit()
 
         # Redecline
@@ -260,11 +416,12 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
+        addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertTrue(addressbook.isShared())
 
+
     @inlineCallbacks
     def test_accept_decline_share(self):
         """
@@ -286,9 +443,7 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(len(notifications), 1)
+        yield self._check_notifications("user02", [inviteUID, ])
 
         yield self.commit()
 
@@ -299,9 +454,8 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is not None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
         yield self.commit()
 
@@ -312,9 +466,7 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
 
     @inlineCallbacks
@@ -338,9 +490,7 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(len(notifications), 1)
+        yield self._check_notifications("user02", [inviteUID, ])
 
         yield self.commit()
 
@@ -351,9 +501,8 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is not None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
         yield self.commit()
 
@@ -364,9 +513,7 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user01")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(notifications, [inviteUID + "-reply", ])
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
 
 
     @inlineCallbacks
@@ -386,7 +533,6 @@
         self.assertEqual(invites[0].uid, shareeView.shareUID())
         self.assertEqual(invites[0].ownerUID, "user01")
         self.assertEqual(invites[0].shareeUID, "user02")
-        self.assertEqual(invites[0].shareeName, shareeView.name())
         self.assertEqual(invites[0].mode, _BIND_MODE_DIRECT)
         self.assertEqual(invites[0].status, _BIND_STATUS_ACCEPTED)
 
@@ -394,9 +540,8 @@
         shared = yield self.addressbookUnderTest(home="user02", name=sharedName)
         self.assertTrue(shared is not None)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(len(notifications), 0)
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_notifications("user02", [])
 
         yield self.commit()
 
@@ -408,9 +553,7 @@
         invites = yield addressbook.sharingInvites()
         self.assertEqual(len(invites), 0)
 
-        notifyHome = yield self.transactionUnderTest().notificationsWithUID("user02")
-        notifications = yield notifyHome.listNotificationObjects()
-        self.assertEqual(len(notifications), 0)
+        yield self._check_notifications("user02", [])
 
 
     @inlineCallbacks
@@ -428,3 +571,678 @@
         addressbook = yield home.addressbookWithName(shared_name)
         self.assertEquals(addressbook.notifierID(), ("CardDAV", "user01/addressbook",))
         yield self.commit()
+
+
+
+class GroupSharing(BaseSharingTests):
+    """
+    Test store-based group book sharing.
+    """
+
+    @inlineCallbacks
+    def test_no_shares(self):
+        """
+        Test that initially there are no shares.
+        """
+
+        addressbook = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        invites = yield addressbook.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+
+    @inlineCallbacks
+    def test_invite_sharee(self):
+        """
+        Test invite/uninvite creates/removes shares and notifications.
+        """
+
+        # Invite
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(group.isShared())
+
+        shareeView = yield group.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertEqual(invites[0].uid, shareeView.shareUID())
+        self.assertEqual(invites[0].ownerUID, "user01")
+        self.assertEqual(invites[0].shareeUID, "user02")
+        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()
+
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user02", [inviteUID, ])
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        # Uninvite
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        self.assertTrue(group.isShared())
+
+        yield group.uninviteUserFromShare("user02")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertTrue(group.isShared())
+
+        yield self._check_notifications("user02", [])
+
+        yield self.commit()
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertFalse(group.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_share(self):
+        """
+        Test that invite+accept creates shares and notifications.
+        """
+
+        # Invite
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(group.isShared())
+
+        shareeView = yield group.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        inviteUID = shareeView.shareUID()
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user02", [inviteUID, ])
+
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        # Accept
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        yield self._check_addressbook("user02", "user01", self.group1_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        # Re-accept
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        yield self._check_addressbook("user02", "user01", self.group1_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertTrue(group.isShared())
+
+
+    @inlineCallbacks
+    def test_decline_share(self):
+        """
+        Test that invite+decline does not create shares but does create notifications.
+        """
+
+        # Invite
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(group.isShared())
+
+        shareeView = yield group.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        inviteUID = shareeView.shareUID()
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user02", [inviteUID, ])
+
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        # Decline
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.declineShare(inviteUID)
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        # Re-decline
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.declineShare(inviteUID)
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertTrue(group.isShared())
+
+
+    @inlineCallbacks
+    def test_accept_decline_share(self):
+        """
+        Test that invite+accept/decline creates/removes shares and notifications.
+        Decline via the home.
+        """
+
+        # Invite
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(group.isShared())
+
+        shareeView = yield group.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        inviteUID = shareeView.shareUID()
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user02", [inviteUID, ])
+
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        # Accept
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        yield self._check_addressbook("user02", "user01", self.group1_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertTrue(group.isShared())
+
+        yield self.commit()
+
+        # Decline
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.declineShare(inviteUID)
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        self.assertTrue(group.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
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        shareeView = yield group.inviteUserToShare("user02", _BIND_MODE_READ, "summary")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 1)
+        inviteUID = shareeView.shareUID()
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user02", [inviteUID, ])
+
+        yield self.commit()
+
+        # Accept
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.acceptShare(inviteUID)
+
+        yield self._check_addressbook("user02", "user01", self.group1_children)
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+        yield self.commit()
+
+        # Delete
+        group = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="group1.vcf")
+        yield group.deleteShare()
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+        yield self._check_notifications("user01", [inviteUID + "-reply", ])
+
+
+    @inlineCallbacks
+    def test_accept_two_groups(self):
+        """
+        Test that accept of two groups works.
+        """
+
+        # Two shares
+        inviteUID1 = yield self._createGroupShare(groupname="group1.vcf")
+        inviteUID2 = yield self._createGroupShare(groupname="group2.vcf")
+
+        yield self._check_addressbook("user02", "user01", self.all_children)
+        yield self._check_notifications("user01", [inviteUID1 + "-reply", inviteUID2 + "-reply", ])
+
+
+    @inlineCallbacks
+    def test_accept_uninvite_two_groups(self):
+        """
+        Test that accept of two groups works, then uninvite each one.
+        """
+
+        # Two shares
+        inviteUID1 = yield self._createGroupShare(groupname="group1.vcf")
+        inviteUID2 = yield self._createGroupShare(groupname="group2.vcf")
+
+        yield self._check_addressbook("user02", "user01", self.all_children)
+        yield self._check_notifications("user01", [inviteUID1 + "-reply", inviteUID2 + "-reply", ])
+
+        yield self.commit()
+
+        # Uninvite one
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        yield group.uninviteUserFromShare("user02")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        yield self._check_addressbook("user02", "user01", self.group2_children)
+
+        shared = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="group1.vcf")
+        self.assertTrue(shared is None)
+        shared = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="card2.vcf")
+        self.assertTrue(shared is None)
+
+        yield self.commit()
+
+        # Uninvite other
+        group = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="group2.vcf")
+        yield group.uninviteUserFromShare("user02")
+        invites = yield group.sharingInvites()
+        self.assertEqual(len(invites), 0)
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+
+    @inlineCallbacks
+    def test_accept_decline_two_groups(self):
+        """
+        Test that accept of two groups works, then decline each one.
+        """
+
+        # Two shares
+        inviteUID1 = yield self._createGroupShare(groupname="group1.vcf")
+        inviteUID2 = yield self._createGroupShare(groupname="group2.vcf")
+
+        yield self._check_addressbook("user02", "user01", self.all_children)
+        yield self._check_notifications("user01", [inviteUID1 + "-reply", inviteUID2 + "-reply", ])
+
+        yield self.commit()
+
+        # Decline one
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.declineShare(inviteUID1)
+
+        yield self._check_addressbook("user02", "user01", self.group2_children)
+
+        shared = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="group1.vcf")
+        self.assertTrue(shared is None)
+        shared = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="card2.vcf")
+        self.assertTrue(shared is None)
+
+        yield self.commit()
+
+        # Decline other
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.declineShare(inviteUID2)
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+
+    @inlineCallbacks
+    def test_accept_two_groups_different_access(self):
+        """
+        Test that accept of two groups works, then uninvite each one.
+        """
+
+        # Two shares
+        inviteUID1 = yield self._createGroupShare(groupname="group1.vcf")
+        inviteUID2 = yield self._createGroupShare(groupname="group2.vcf", mode=_BIND_MODE_WRITE)
+
+        yield self._check_addressbook("user02", "user01", self.all_children)
+        yield self._check_notifications("user01", [inviteUID1 + "-reply", inviteUID2 + "-reply", ])
+
+        # Read only for all, write for group2's items
+        yield self._check_read_only("user02", "user01", ["group1.vcf", "card2.vcf", ])
+        yield self._check_read_write("user02", "user01", ["group2.vcf", "card1.vcf", "card3.vcf", ])
+
+        yield self.commit()
+
+        # Decline one
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.declineShare(inviteUID2)
+
+        yield self._check_addressbook("user02", "user01", self.group1_children)
+
+        yield self._check_read_only("user02", "user01", ["group1.vcf", "card1.vcf", "card2.vcf", ])
+
+        shared = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="group2.vcf")
+        self.assertTrue(shared is None)
+        shared = yield self.addressbookObjectUnderTest(home="user02", addressbook_name="user01", name="card3.vcf")
+        self.assertTrue(shared is None)
+
+        yield self.commit()
+
+        # Decline other
+        shareeHome = yield self.addressbookHomeUnderTest(name="user02")
+        yield shareeHome.declineShare(inviteUID1)
+
+        sharedParent = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertTrue(sharedParent is None)
+
+
+
+class MixedSharing(BaseSharingTests):
+    """
+    Test store-based combined address book and group book sharing.
+    """
+
+    @inlineCallbacks
+    def test_addressbook_ro_then_groups(self):
+
+        # Share address book read-only
+        shareeName = yield self._createShare()
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_read_only("user02", "user01", self.all_children)
+        yield self._check_read_write("user02", "user01", [])
+        yield self._check_notifications("user02", [shareeName, ])
+
+        # Add group1 read-write
+        inviteUID1 = yield self._createGroupShare(groupname="group1.vcf", mode=_BIND_MODE_WRITE)
+
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_read_only("user02", "user01", ["group2.vcf", "card3.vcf", ])
+        yield self._check_read_write("user02", "user01", ["group1.vcf", "card1.vcf", "card2.vcf", ])
+        yield self._check_notifications("user02", [shareeName, inviteUID1, ])
+
+        # Add group2 read-write
+        inviteUID2 = yield self._createGroupShare(groupname="group2.vcf", mode=_BIND_MODE_WRITE)
+
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_read_only("user02", "user01", [])
+        yield self._check_read_write("user02", "user01", self.all_children)
+        yield self._check_notifications("user02", [shareeName, inviteUID1, inviteUID2])
+
+        # Uninvite group1
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        yield group.uninviteUserFromShare("user02")
+
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_read_only("user02", "user01", ["group1.vcf", "card2.vcf", ])
+        yield self._check_read_write("user02", "user01", ["group2.vcf", "card1.vcf", "card3.vcf", ])
+
+        # Uninvite group2
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group2.vcf")
+        yield group.uninviteUserFromShare("user02")
+
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_read_only("user02", "user01", self.all_children)
+        yield self._check_read_write("user02", "user01", [])
+
+
+    @inlineCallbacks
+    def test_addressbook_ro_then_group_no_accept(self):
+
+        # Share address book read-only
+        shareeName = yield self._createShare()
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_read_only("user02", "user01", self.all_children)
+        yield self._check_read_write("user02", "user01", [])
+        yield self._check_notifications("user02", [shareeName, ])
+
+        # Add group1 read-write - but do not accept
+        group = yield self.addressbookObjectUnderTest(home="user01", addressbook_name="addressbook", name="group1.vcf")
+        invited = yield group.inviteUserToShare("user02", _BIND_MODE_WRITE, "summary")
+        yield self._check_notifications("user02", [shareeName, invited.shareUID(), ])
+
+        yield self._check_addressbook("user02", "user01", self.fully_shared_children)
+        yield self._check_read_only("user02", "user01", self.all_children)
+        yield self._check_read_write("user02", "user01", [])
+
+
+
+class SharingRevisions(BaseSharingTests):
+    """
+    Test store-based sharing and interaction with revision table.
+    """
+
+    @inlineCallbacks
+    def test_shareWithRevision(self):
+        """
+        Verify that bindRevision on addressbooks and shared addressbooks has the correct value.
+        """
+        yield self._createShare()
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertNotEqual(otherAB._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_shareGroupWithRevision(self):
+        """
+        Verify that bindRevision on addressbooks and shared groups has the correct value.
+        """
+
+        yield self._createGroupShare(groupname="group1.vcf")
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertNotEqual(otherAB._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_updateShareRevision(self):
+        """
+        Verify that bindRevision on addressbooks and shared addressbooks has the correct value.
+        """
+        newABShareUID = yield self._inviteShare()
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherHome = yield self.addressbookHomeUnderTest(name="user02")
+        otherAB = yield otherHome.anyObjectWithShareUID("user01")
+        self.assertEqual(otherAB._bindRevision, 0)
+        yield self.commit()
+
+        yield self._acceptShare(newABShareUID)
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertNotEqual(otherAB._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_updateSharedGroupRevision(self):
+        """
+        Verify that bindRevision on addressbooks and shared addressbooks has the correct value.
+        """
+        newGroupShareUID = yield self._inviteGroupShare(groupname="group1.vcf")
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherHome = yield self.addressbookHomeUnderTest(name="user02")
+        otherAB = yield otherHome.anyObjectWithShareUID("user01")
+        self.assertEqual(otherAB._bindRevision, 0)
+        yield self.commit()
+
+        yield self._acceptGroupShare(newGroupShareUID)
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertNotEqual(otherAB._bindRevision, 0)
+
+
+#    @inlineCallbacks
+    def test_sharedRevisions(self):
+        """
+        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+        """
+
+        yield self._createShare()
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertNotEqual(otherAB._bindRevision, 0)
+
+        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision - 1)
+        self.assertNotEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
+        self.assertEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        otherHome = yield self.addressbookHomeUnderTest(name="user02")
+        for depth in ("1", "infinity",):
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
+            self.assertNotEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+
+
+    @inlineCallbacks
+    def test_sharedGroupRevisions(self):
+        """
+        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+        """
+
+        yield self._createGroupShare("group1.vcf")
+
+        normalAB = yield self.addressbookUnderTest(home="user01", name="addressbook")
+        self.assertEqual(normalAB._bindRevision, 0)
+        otherAB = yield self.addressbookUnderTest(home="user02", name="user01")
+        self.assertNotEqual(otherAB._bindRevision, 0)
+
+        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision - 1)
+        self.assertNotEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        changed, deleted = yield otherAB.resourceNamesSinceRevision(otherAB._bindRevision)
+        self.assertEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        otherHome = yield self.addressbookHomeUnderTest(name="user02")
+        for depth in ("1", "infinity",):
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision - 1, depth)
+            self.assertNotEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherAB._bindRevision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+
+
+    @inlineCallbacks
+    def test_addressbookRevisionChangeConcurrency(self):
+        """
+        Test that two concurrent attempts to add resources in two separate
+        calendar homes does not deadlock on the revision table update.
+        """
+
+        # Create first events in different addressbook homes
+        txn1 = self._sqlStore.newTransaction()
+        txn2 = self._sqlStore.newTransaction()
+
+        addressbook_uid1_in_txn1 = yield self.addressbookUnderTest(txn1, "addressbook", "user01")
+        addressbook_uid2_in_txn2 = yield self.addressbookUnderTest(txn2, "addressbook", "user02")
+
+        data = """BEGIN:VCARD
+VERSION:3.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+UID:data%(ctr)s
+FN:Data %(ctr)s
+N:Sub Group;;;;
+REV:20120503T194243Z
+END:VCARD
+
+"""
+
+        component = Component.fromString(data % {"ctr": 1})
+        yield addressbook_uid1_in_txn1.createAddressBookObjectWithName("data1.ics", component)
+
+        component = Component.fromString(data % {"ctr": 2})
+        yield addressbook_uid2_in_txn2.createAddressBookObjectWithName("data2.ics", component)
+
+        # Setup deferreds to run concurrently and create second events in the calendar homes
+        # previously used by the other transaction - this could create the deadlock.
+        @inlineCallbacks
+        def _defer_uid3():
+            addressbook_uid1_in_txn2 = yield self.addressbookUnderTest(txn2, "addressbook", "user01")
+            component = Component.fromString(data % {"ctr": 3})
+            yield addressbook_uid1_in_txn2.createAddressBookObjectWithName("data3.ics", component)
+            yield txn2.commit()
+        d1 = _defer_uid3()
+
+        @inlineCallbacks
+        def _defer_uid4():
+            addressbook_uid2_in_txn1 = yield self.addressbookUnderTest(txn1, "addressbook", "user02")
+            component = Component.fromString(data % {"ctr": 4})
+            yield addressbook_uid2_in_txn1.createAddressBookObjectWithName("data4.ics", component)
+            yield txn1.commit()
+        d2 = _defer_uid4()
+
+        # Now do the concurrent provision attempt
+        yield DeferredList([d1, d2])
+
+        # Verify we did not have a deadlock and all resources have been created.
+        vcarddata1 = yield self.addressbookObjectUnderTest(name="data1.ics", addressbook_name="addressbook", home="user01")
+        vcarddata2 = yield self.addressbookObjectUnderTest(name="data2.ics", addressbook_name="addressbook", home="user02")
+        vcarddata3 = yield self.addressbookObjectUnderTest(name="data3.ics", addressbook_name="addressbook", home="user01")
+        vcarddata4 = yield self.addressbookObjectUnderTest(name="data4.ics", addressbook_name="addressbook", home="user02")
+        self.assertNotEqual(vcarddata1, None)
+        self.assertNotEqual(vcarddata2, None)
+        self.assertNotEqual(vcarddata3, None)
+        self.assertNotEqual(vcarddata4, None)

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql.py	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql.py	2013-11-23 21:41:29 UTC (rev 12007)
@@ -1842,28 +1842,6 @@
         return self._childClass.objectWithName(self, name)
 
 
-    def objectWithShareUID(self, shareUID):
-        """
-        Retrieve the child with the given bind identifier contained in this
-        home.
-
-        @param name: a string.
-        @return: an L{ICalendar} or C{None} if no such child exists.
-        """
-        return self.childWithName(shareUID)
-
-
-    def invitedObjectWithShareUID(self, shareUID):
-        """
-        Retrieve the child invitation with the given bind identifier contained in this
-        home.
-
-        @param name: a string.
-        @return: an L{ICalendar} or C{None} if no such child exists.
-        """
-        return self._childClass.objectWithName(self, shareUID, accepted=False)
-
-
     def anyObjectWithShareUID(self, shareUID):
         """
         Retrieve the child accepted or otherwise with the given bind identifier contained in this
@@ -2793,7 +2771,7 @@
 
 SharingInvitation = namedtuple(
     "SharingInvitation",
-    ["uid", "ownerUID", "shareeUID", "shareeName", "mode", "status", "summary"]
+    ["uid", "ownerUID", "ownerHomeID", "shareeUID", "shareeHomeID", "mode", "status", "summary"]
 )
 
 
@@ -2962,26 +2940,6 @@
 
 
     @inlineCallbacks
-    def indirectShareWithUser(self, shareeUID):
-        """
-        Create a indirect share with the specified user. An indirect share is one created as a
-        side-effect of some other object being shared.
-
-        NB no invitations are used with indirect sharing.
-
-        @param shareeUID: UID of the sharee
-        @type shareeUID: C{str}
-        """
-
-        # Ignore if it already exists
-        shareeView = yield self.shareeView(shareeUID)
-        if shareeView is None:
-            shareeView = yield self.createShare(shareeUID=shareeUID, mode=_BIND_MODE_INDIRECT)
-            yield shareeView.newShare()
-        returnValue(shareeView)
-
-
-    @inlineCallbacks
     def uninviteUserFromShare(self, shareeUID):
         """
         Remove a user from a share. Make sure a notification is sent as well.
@@ -2995,7 +2953,7 @@
         if shareeView is not None:
             # If current user state is accepted then we send an invite with the new state, otherwise
             # we cancel any existing invites for the user
-            if shareeView.useInvite():
+            if not shareeView.direct():
                 if shareeView.shareStatus() != _BIND_STATUS_ACCEPTED:
                     yield self._removeInviteNotification(shareeView)
                 else:
@@ -3011,7 +2969,7 @@
         This share is being accepted.
         """
 
-        if self.useInvite() and self.shareStatus() != _BIND_STATUS_ACCEPTED:
+        if not self.direct() and self.shareStatus() != _BIND_STATUS_ACCEPTED:
             ownerView = yield self.ownerView()
             yield ownerView.updateShare(self, status=_BIND_STATUS_ACCEPTED)
             yield self.newShare(displayname=summary)
@@ -3024,7 +2982,7 @@
         This share is being declined.
         """
 
-        if self.useInvite() and self.shareStatus() != _BIND_STATUS_DECLINED:
+        if not self.direct() and self.shareStatus() != _BIND_STATUS_DECLINED:
             ownerView = yield self.ownerView()
             yield ownerView.updateShare(self, status=_BIND_STATUS_DECLINED)
             yield self._sendReplyNotification(ownerView)
@@ -3037,10 +2995,10 @@
         """
 
         ownerView = yield self.ownerView()
-        if self.useInvite():
-            yield self.declineShare()
-        else:
+        if self.direct():
             yield ownerView.removeShare(self)
+        else:
+            yield self.declineShare()
 
 
     def newShare(self, displayname=None):
@@ -3058,8 +3016,8 @@
         """
         invitations = yield self.sharingInvites()
 
-        # remove direct/indirect shares as those are not "real" invitations
-        invitations = filter(lambda x: x.mode not in (_BIND_MODE_DIRECT, _BIND_MODE_INDIRECT), invitations)
+        # remove direct shares as those are not "real" invitations
+        invitations = filter(lambda x: x.mode != _BIND_MODE_DIRECT, invitations)
         invitations.sort(key=lambda invitation: invitation.shareeUID)
         returnValue(invitations)
 
@@ -3071,13 +3029,9 @@
         """
 
         # When deleting the message is the sharee's display name
+        displayname = shareeView.shareMessage()
         if notificationState == _BIND_STATUS_DELETED:
-            try:
-                displayname = str(shareeView.properties()[PropertyName.fromElement(element.DisplayName)])
-            except KeyError:
-                displayname = shareeView.name()
-        else:
-            displayname = shareeView.shareMessage()
+            displayname = str(shareeView.properties().get(PropertyName.fromElement(element.DisplayName), displayname))
 
         notificationtype = {
             "notification-type": "invite-notification",
@@ -3148,7 +3102,30 @@
     #
     # Lower level API
     #
+
     @inlineCallbacks
+    def ownerView(self):
+        """
+        Return the owner resource counterpart of this shared resource.
+        """
+        # Get the child of the owner home that has the same resource id as the owned one
+        ownerView = yield self.ownerHome().childWithID(self.id())
+        returnValue(ownerView)
+
+
+    @inlineCallbacks
+    def shareeView(self, shareeUID):
+        """
+        Return the shared resource counterpart of this owned resource for the specified sharee.
+        """
+
+        # Get the child of the sharee home that has the same resource id as the owned one
+        shareeHome = yield self._txn.homeWithUID(self._home._homeType, shareeUID)
+        shareeView = (yield shareeHome.allChildWithID(self.id())) if shareeHome is not None else None
+        returnValue(shareeView)
+
+
+    @inlineCallbacks
     def shareWith(self, shareeHome, mode, status=None, summary=None):
         """
         Share this (owned) L{CommonHomeChild} with another home.
@@ -3179,18 +3156,20 @@
         def doInsert(subt):
             newName = self.newShareName()
             yield self._bindInsertQuery.on(
-                subt, homeID=shareeHome._resourceID,
-                resourceID=self._resourceID, name=newName,
-                mode=mode, bindStatus=status, message=summary
+                subt,
+                homeID=shareeHome._resourceID,
+                resourceID=self._resourceID,
+                name=newName,
+                mode=mode,
+                bindStatus=status,
+                message=summary
             )
             returnValue(newName)
         try:
             bindName = yield self._txn.subtransaction(doInsert)
         except AllRetriesFailed:
             # FIXME: catch more specific exception
-            child = yield shareeHome.childWithID(self._resourceID)
-            if not child:
-                child = yield shareeHome.objectWithID(shareeHome, self._resourceID, accepted=False)
+            child = yield shareeHome.allChildWithID(self._resourceID)
             yield self.updateShare(
                 child, mode=mode, status=status,
                 summary=summary
@@ -3198,7 +3177,7 @@
             bindName = child._name
         else:
             if status == _BIND_STATUS_ACCEPTED:
-                shareeView = yield shareeHome.objectWithShareUID(bindName)
+                shareeView = yield shareeHome.anyObjectWithShareUID(bindName)
                 yield shareeView._initSyncToken()
                 yield shareeView._initBindRevision()
 
@@ -3213,28 +3192,6 @@
 
 
     @inlineCallbacks
-    def ownerView(self):
-        """
-        Return the owner resource counterpart of this shared resource.
-        """
-        # Get the child of the owner home that has the same resource id as the owned one
-        ownerView = yield self.ownerHome().childWithID(self.id())
-        returnValue(ownerView)
-
-
-    @inlineCallbacks
-    def shareeView(self, shareeUID):
-        """
-        Return the shared resource counterpart of this owned resource for the specified sharee.
-        """
-
-        # Get the child of the sharee home that has the same resource id as the owned one
-        shareeHome = yield self._txn.homeWithUID(self._home._homeType, shareeUID)
-        shareeView = (yield shareeHome.allChildWithID(self.id())) if shareeHome is not None else None
-        returnValue(shareeView)
-
-
-    @inlineCallbacks
     def createShare(self, shareeUID, mode, summary=None):
         """
         Create a new shared resource. If the mode is direct, the share is created in accepted state,
@@ -3245,7 +3202,7 @@
         yield self.shareWith(
             shareeHome,
             mode=mode,
-            status=_BIND_STATUS_INVITED if mode not in (_BIND_MODE_DIRECT, _BIND_MODE_INDIRECT) else _BIND_STATUS_ACCEPTED,
+            status=_BIND_STATUS_INVITED if mode != _BIND_MODE_DIRECT else _BIND_STATUS_ACCEPTED,
             summary=summary,
         )
         shareeView = yield self.shareeView(shareeUID)
@@ -3290,6 +3247,10 @@
 
         if len(columnMap):
 
+            # Count accepted
+            if status is not None:
+                previouslyAcceptedCount = yield shareeView._previousAcceptCount()
+
             yield self._updateBindColumnsQuery(columnMap).on(
                 self._txn,
                 resourceID=self._resourceID, homeID=shareeView._home._resourceID
@@ -3301,15 +3262,7 @@
 
             if status is not None:
                 shareeView._bindStatus = columnMap[bind.BIND_STATUS]
-                if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
-                    yield shareeView._initSyncToken()
-                    yield shareeView._initBindRevision()
-                    shareeView._home._children[shareeView._name] = shareeView
-                    shareeView._home._children[shareeView._resourceID] = shareeView
-                elif shareeView._bindStatus == _BIND_STATUS_DECLINED:
-                    yield shareeView._deletedSyncToken(sharedRemoval=True)
-                    shareeView._home._children.pop(shareeView._name, None)
-                    shareeView._home._children.pop(shareeView._resourceID, None)
+                yield shareeView._changedStatus(previouslyAcceptedCount)
 
             if summary is not None:
                 shareeView._bindMessage = columnMap[bind.MESSAGE]
@@ -3326,7 +3279,24 @@
             yield shareeView.viewerHome().notifyChanged()
 
 
+    def _previousAcceptCount(self):
+        return succeed(1)
+
+
     @inlineCallbacks
+    def _changedStatus(self, previouslyAcceptedCount):
+        if self._bindStatus == _BIND_STATUS_ACCEPTED:
+            yield self._initSyncToken()
+            yield self._initBindRevision()
+            self._home._children[self._name] = self
+            self._home._children[self._resourceID] = self
+        elif self._bindStatus in (_BIND_STATUS_INVITED, _BIND_STATUS_DECLINED):
+            yield self._deletedSyncToken(sharedRemoval=True)
+            self._home._children.pop(self._name, None)
+            self._home._children.pop(self._resourceID, None)
+
+
+    @inlineCallbacks
     def removeShare(self, shareeView):
         """
         Remove the shared version of this (owned) L{CommonHomeChild} from the
@@ -3350,7 +3320,11 @@
         yield shareeHome.notifyChanged()
 
         # delete binds including invites
-        yield self._deleteBindForResourceIDAndHomeID.on(self._txn, resourceID=self._resourceID, homeID=shareeHome._resourceID)
+        yield self._deleteBindForResourceIDAndHomeID.on(
+            self._txn,
+            resourceID=self._resourceID,
+            homeID=shareeHome._resourceID,
+        )
 
         queryCacher = self._txn._queryCacher
         if queryCacher:
@@ -3373,8 +3347,8 @@
                 yield self.removeShare(shareeView)
         else:
             # This collection is shared to me
-            ownerHomeChild = yield self.ownerHome().childWithID(self._resourceID)
-            yield ownerHomeChild.removeShare(self)
+            ownerView = yield self.ownerView()
+            yield ownerView.removeShare(self)
 
 
     @inlineCallbacks
@@ -3398,8 +3372,9 @@
             invite = SharingInvitation(
                 resourceName,
                 self.ownerHome().name(),
+                self.ownerHome().id(),
                 homeUID,
-                resourceName,
+                homeRID,
                 bindMode,
                 bindStatus,
                 bindMessage,
@@ -3507,15 +3482,6 @@
         return self._bindMode == _BIND_MODE_INDIRECT
 
 
-    def useInvite(self):
-        """
-        Does this type of share use invitations?
-
-        @return: a boolean indicating whether invitations are used.
-        """
-        return self._bindMode not in (_BIND_MODE_DIRECT, _BIND_MODE_INDIRECT)
-
-
     def shareUID(self):
         """
         @see: L{ICalendar.shareUID}
@@ -3550,6 +3516,13 @@
         return self._bindStatus
 
 
+    def accepted(self):
+        """
+        @see: L{ICalendar.shareStatus}
+        """
+        return self._bindStatus == _BIND_STATUS_ACCEPTED
+
+
     def shareMessage(self):
         """
         @see: L{ICalendar.shareMessage}
@@ -3818,17 +3791,17 @@
 
     @classmethod
     def objectWithName(cls, home, name, accepted=True):
-        return cls.objectWithNameOrID(home, name=name, accepted=accepted)
+        return cls._objectWithNameOrID(home, name=name, accepted=accepted)
 
 
     @classmethod
     def objectWithID(cls, home, resourceID, accepted=True):
-        return cls.objectWithNameOrID(home, resourceID=resourceID, accepted=accepted)
+        return cls._objectWithNameOrID(home, resourceID=resourceID, accepted=accepted)
 
 
     @classmethod
     @inlineCallbacks
-    def objectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
+    def _objectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
         # replaces objectWithName()
         """
         Retrieve the child with the given C{name} or C{resourceID} contained in the given
@@ -3984,9 +3957,7 @@
             dataRows = None
         if dataRows is None:
             # No cached copy
-            dataRows = (
-                yield self._metadataByIDQuery.on(self._txn,
-                    resourceID=self._resourceID))[0]
+            dataRows = (yield self._metadataByIDQuery.on(self._txn, resourceID=self._resourceID))[0]
             if queryCacher:
                 # Cache the results
                 yield queryCacher.setAfterCommit(self._txn, cacheKey, dataRows)
@@ -4557,8 +4528,7 @@
 
 
     @inlineCallbacks
-    def _notifyChanged(self, property_change=False,
-            category=ChangeCategory.default):
+    def _notifyChanged(self, property_change=False, category=ChangeCategory.default):
         """
         Send notifications, change sync token and bump last modified because
         the resource has changed.  We ensure we only do this once per object
@@ -4992,6 +4962,18 @@
         return self._parentCollection
 
 
+    def owned(self):
+        return self._parentCollection.owned()
+
+
+    def ownerHome(self):
+        return self._parentCollection.ownerHome()
+
+
+    def viewerHome(self):
+        return self._parentCollection.viewerHome()
+
+
     @classmethod
     def _selectForUpdateQuery(cls, nowait): #@NoSelf
         """
@@ -5366,12 +5348,16 @@
         yield notificationObject.setData(uid, notificationtype, notificationdata, inserting=inserting)
         if inserting:
             yield self._insertRevision("%s.xml" % (uid,))
+            if self._notificationNames is not None:
+                self._notificationNames.append(notificationObject.uid())
         else:
             yield self._updateRevision("%s.xml" % (uid,))
         yield self.notifyChanged()
 
 
     def removeNotificationObjectWithName(self, name):
+        if self._notificationNames is not None:
+            self._notificationNames.remove(self._nameToUID(name))
         return self.removeNotificationObjectWithUID(self._nameToUID(name))
 
     _removeByUIDQuery = Delete(

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-11-23 21:41:29 UTC (rev 12007)
@@ -58,7 +58,7 @@
     "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
     "NOTIFICATION_UID" nvarchar2(255),
     "NOTIFICATION_TYPE" nvarchar2(255),
-    "XML_DATA" nclob,
+    "NOTIFICATION_DATA" nclob,
     "MD5" nchar(32),
     "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
     "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
@@ -92,6 +92,7 @@
 insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('read', 1);
 insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('write', 2);
 insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('direct', 3);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
 create table CALENDAR_BIND_STATUS (
     "ID" integer primary key,
     "DESCRIPTION" nvarchar2(16) unique

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current.sql	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/current.sql	2013-11-23 21:41:29 UTC (rev 12007)
@@ -702,7 +702,7 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '30');
+insert into CALENDARSERVER values ('VERSION', '29');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '5');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_28_to_29.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_28_to_29.sql	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_28_to_29.sql	2013-11-23 21:41:29 UTC (rev 12007)
@@ -18,7 +18,7 @@
 -- Upgrade database schema from VERSION 28 to 29 --
 ---------------------------------------------------
 
--- Push notification work related updates
+-- Sharing notification related updates
 
 alter table NOTIFICATION_HOME
  add ("DATAVERSION" integer default 0 not null);
@@ -28,5 +28,11 @@
 alter table NOTIFICATION
   rename column XML_DATA to NOTIFICATION_DATA;
 
+  -- Sharing enumeration updates
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
+
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('deleted', 4);
+
 -- Now update the version
 update CALENDARSERVER set VALUE = '29' where NAME = 'VERSION';
+insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');

Modified: CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_28_to_29.sql
===================================================================
--- CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_28_to_29.sql	2013-11-23 05:43:57 UTC (rev 12006)
+++ CalendarServer/branches/users/cdaboo/sharing-in-the-store/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_28_to_29.sql	2013-11-23 21:41:29 UTC (rev 12007)
@@ -18,7 +18,7 @@
 -- Upgrade database schema from VERSION 28 to 29 --
 ---------------------------------------------------
 
--- Push notification work related updates
+-- Sharing notification related updates
 
 alter table NOTIFICATION_HOME
   add column DATAVERSION integer default 0 not null;
@@ -28,5 +28,11 @@
 alter table NOTIFICATION
   rename column XML_DATA to NOTIFICATION_DATA;
 
+-- Sharing enumeration updates
+insert into CALENDAR_BIND_MODE values (4, 'indirect');
+
+insert into CALENDAR_BIND_STATUS values (4, 'deleted');
+
 -- Now update the version
 update CALENDARSERVER set VALUE = '29' where NAME = 'VERSION';
+insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/c1f60cf2/attachment.html>


More information about the calendarserver-changes mailing list