[CalendarServer-changes] [9325] CalendarServer/branches/users/gaya/sharedabgroups

source_changes at macosforge.org source_changes at macosforge.org
Fri Jun 1 18:11:12 PDT 2012


Revision: 9325
          http://trac.macosforge.org/projects/calendarserver/changeset/9325
Author:   gaya at apple.com
Date:     2012-06-01 18:11:11 -0700 (Fri, 01 Jun 2012)
Log Message:
-----------
First working filtered group address books

Modified Paths:
--------------
    CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/sharing.py
    CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/storebridge.py
    CalendarServer/branches/users/gaya/sharedabgroups/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_legacy.py
    CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_schema/current.sql

Modified: CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/sharing.py	2012-06-01 19:33:48 UTC (rev 9324)
+++ CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/sharing.py	2012-06-02 01:11:11 UTC (rev 9325)
@@ -32,7 +32,7 @@
 from twext.web2.dav.util import allDataFromStream, joinURL
 from txdav.xml import element
 
-from twisted.internet.defer import succeed, inlineCallbacks, DeferredList,\
+from twisted.internet.defer import inlineCallbacks, DeferredList,\
     returnValue
 
 from twistedcaldav import customxml, caldavxml
@@ -75,10 +75,11 @@
     def upgradeToShare(self):
         """ Upgrade this collection to a shared state """
         
-        # Change resourcetype
-        rtype = self.resourceType()
-        rtype = element.ResourceType(*(rtype.children + (customxml.SharedOwner(),)))
-        self.writeDeadProperty(rtype)
+        if self.isCollection():
+        	# Change resourcetype
+            rtype = self.resourceType()
+            rtype = element.ResourceType(*(rtype.children + (customxml.SharedOwner(),)))
+            self.writeDeadProperty(rtype)
         
         # Create invites database
         self.invitesDB().create()
@@ -202,7 +203,10 @@
     @inlineCallbacks
     def isShared(self, request):
         """ Return True if this is an owner shared calendar collection """
-        returnValue((yield self.isSpecialCollection(customxml.SharedOwner)))
+        if self.isCollection():
+            returnValue((yield self.isSpecialCollection(customxml.SharedOwner)))
+        else:
+            returnValue((yield self.isSharedGroup()))
 
 
     def setVirtualShare(self, shareePrincipal, share):
@@ -211,7 +215,11 @@
         self._share = share
 
         if hasattr(self, "_newStoreObject"):
-            self._newStoreObject.setSharingUID(self._shareePrincipal.principalUID())
+            if hasattr(self._newStoreObject, "setSharingUID"): # FIXME:  skip this for now for groups
+                self._newStoreObject.setSharingUID(self._shareePrincipal.principalUID())
+            else:
+                print("xxx setVirtualShare() self=%s, shareePrincipal=%s, share=%s: skipping setSharingUID() " % (self, shareePrincipal, share,))
+               
 
 
     def isVirtualShare(self):
@@ -847,17 +855,48 @@
         customxml.InviteReply: _xmlHandleInviteReply,          
     }
 
+    @inlineCallbacks
+    def isGroup(self):
+        try:
+            vCard = (yield self.vCard())
+        except AttributeError:
+            pass
+        else:
+            self.log_info("vCard = %s" % (vCard,))
+            if vCard.propertyValue("X-ADDRESSBOOKSERVER-KIND") == "group":
+                self.log_info("isGroup() returning True")
+                returnValue(True)
+        
+        self.log_info("isGroup() returning False")
+        returnValue(False)
+    
+
+    @inlineCallbacks
     def POST_handler_content_type(self, request, contentType):
         if self.isCollection():
             if contentType:
                 if contentType in self._postHandlers:
-                    return self._postHandlers[contentType](self, request)
+                    returnValue((yield self._postHandlers[contentType](self, request)))
                 else:
                     self.log_info("Get a POST of an unsupported content type on a collection type: %s" % (contentType,))
             else:
                 self.log_info("Get a POST with no content type on a collection")
-        return succeed(responsecode.FORBIDDEN)
+        elif (yield self.isGroup()):
+            if contentType:
+                if contentType in self._postHandlers:
+                    returnValue((yield self._postHandlers[contentType](self, request)))
+                else:
+                    self.log_info("Get a POST of an unsupported content type on a group type: %s" % (contentType,))
+            else:
+                self.log_info("Get a POST with no content type on a group")
+        
+        else:
+            self.log_info("Got POST on non-collection, non-group object: %s" % (self,))
+            
+        returnValue(responsecode.FORBIDDEN)
 
+
+
     _postHandlers = {
         ("application", "xml") : xmlRequestHandler,
         ("text", "xml") : xmlRequestHandler,
@@ -1113,8 +1152,12 @@
         
         # Set per-user displayname or color to whatever was given
         sharedCollection.setVirtualShare(ownerPrincipal, share)
-        if displayname:
-            yield sharedCollection.writeProperty(element.DisplayName.fromString(displayname), request)
+        if sharedCollection.isCollection():
+            if displayname:
+                yield sharedCollection.writeProperty(element.DisplayName.fromString(displayname), request)
+        else:
+            print("xxx _acceptShare skipping set of display name")
+        
         if color:
             yield sharedCollection.writeProperty(customxml.CalendarColor.fromString(color), request)
 

Modified: CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/storebridge.py	2012-06-01 19:33:48 UTC (rev 9324)
+++ CalendarServer/branches/users/gaya/sharedabgroups/twistedcaldav/storebridge.py	2012-06-02 01:11:11 UTC (rev 9325)
@@ -2160,7 +2160,19 @@
 
     vCard = _CommonObjectResource.component
 
+    def isSharedGroup(self):
+        print("xxx isSharedGroup: self = %s, self._newStoreObject=%s" % (self, self._newStoreObject, ))
+    
+        return self._newStoreObject and self._newStoreObject.isSharedGroup()
 
+    def invitesDB(self):
+        """
+        Retrieve the new-style invites DB wrapper.
+        """
+        if not hasattr(self, "_invitesDB"):
+            self._invitesDB = self._newStoreObject.retrieveOldInvites()
+        return self._invitesDB
+
 class _NotificationChildHelper(object):
     """
     Methods for things which are like notification objects.

Modified: CalendarServer/branches/users/gaya/sharedabgroups/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/txdav/carddav/datastore/sql.py	2012-06-01 19:33:48 UTC (rev 9324)
+++ CalendarServer/branches/users/gaya/sharedabgroups/txdav/carddav/datastore/sql.py	2012-06-02 01:11:11 UTC (rev 9325)
@@ -40,7 +40,7 @@
 
 from txdav.common.datastore.sql_legacy import \
     PostgresLegacyABIndexEmulator, SQLLegacyAddressBookInvites,\
-    SQLLegacyAddressBookShares
+    SQLLegacyAddressBookShares, SQLLegacySharedGroupInvites
 
 from txdav.carddav.datastore.util import validateAddressBookComponent
 from txdav.carddav.iaddressbookstore import IAddressBookHome, IAddressBook,\
@@ -60,8 +60,26 @@
     ADDRESSBOOK_OBJECT_REVISIONS_AND_BIND_TABLE, schema
 from txdav.base.propertystore.base import PropertyName
 
+from twext.internet.decorate import memoizedKey
+from twisted.internet.defer import succeed
+from txdav.common.icommondatastore import HomeChildNameNotAllowedError
 
+from txdav.base.propertystore.sql import PropertyStore
+from txdav.common.datastore.sql_tables import _BIND_MODE_OWN, \
+    _BIND_STATUS_ACCEPTED
 
+from twext.enterprise.dal.syntax import Delete, utcNowSQL
+from twext.enterprise.dal.syntax import Insert
+from twext.enterprise.dal.syntax import Max
+from twext.enterprise.dal.syntax import Parameter
+from twext.enterprise.dal.syntax import Select
+from twext.enterprise.dal.syntax import Update
+
+from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
+    HomeChildNameAlreadyExistsError, NoSuchHomeChildError, ObjectResourceNameAlreadyExistsError, \
+    NoSuchObjectResourceError
+from twext.python.clsprop import classproperty
+
 class AddressBookHome(CommonHome):
 
     implements(IAddressBookHome)
@@ -218,8 +236,429 @@
         """
         return MimeType.fromString("text/vcard; charset=utf-8")
 
+    @classmethod
+    @inlineCallbacks
+    def loadAllObjects(cls, home, owned):
+        print("xxx AddressBook.loadAllObjects() cls=%s" % (cls,))
+        """
+        Load all child objects and return 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 = []
 
+        # Load from the main table first
+        if owned:
+            query = cls._ownedHomeChildrenQuery
+        else:
+            query = cls._sharedHomeChildrenQuery
+        dataRows = (yield query.on(home._txn, resourceID=home._resourceID))
+        print("xxx AddressBook.loadAllObjects() dataRows=%s" % (dataRows,))
 
+        if dataRows:
+            # Get property stores for all these child resources (if any found)
+            propertyStores = (yield PropertyStore.forMultipleResources(
+                home.uid(), home._txn,
+                cls._bindSchema.RESOURCE_ID, cls._bindSchema.HOME_RESOURCE_ID,
+                home._resourceID
+            ))
+
+            bind = cls._bindSchema
+            rev = cls._revisionsSchema
+            if owned:
+                ownedCond = bind.BIND_MODE == _BIND_MODE_OWN
+            else:
+                ownedCond = bind.BIND_MODE != _BIND_MODE_OWN
+            revisions = (yield Select(
+                [rev.RESOURCE_ID, Max(rev.REVISION)],
+                From=rev.join(bind, rev.RESOURCE_ID == bind.RESOURCE_ID, 'left'),
+                Where=(bind.HOME_RESOURCE_ID == home._resourceID).
+                    And(ownedCond).
+                    And((rev.RESOURCE_NAME != None).Or(rev.DELETED == False)),
+                GroupBy=rev.RESOURCE_ID
+            ).on(home._txn))
+            revisions = dict(revisions)
+            
+        
+ 
+        # Create the actual objects merging in properties
+        for items in dataRows:
+            resourceID, resource_name, groupBindID = items[:3]
+            
+            print("xxx AddressBook.loadAllObjects() groupBindID=%s" % (groupBindID,))
+            
+            child = None
+            if not owned:
+                
+                bind = schema.GROUP_BIND
+                groupIDRows = (yield Select([bind.GROUP_ID,],
+                     From=bind,
+                     Where=(bind.ADDRESSBOOK_BIND_ID == groupBindID)).on(home._txn))
+                
+                print("xxx AddressBook.loadAllObjects() groupIDRows=%s" % (groupIDRows,))
+                
+                if groupIDRows:
+                    [groupIDs] = groupIDRows
+                    
+                    for groupID in groupIDs:
+                        
+                        #debug, print group members
+                        bind = schema.GROUP_MEMBERSHIP
+                        memberRows = (yield Select([bind.MEMBER_ID,],
+                                      From=bind,
+                                      Where=bind.GROUP_ID == groupID).on(home._txn))
+                    
+                        print("xxx AddressBook.loadAllObjects() for groupID=%s" % (memberRows,))
+                   
+                        child = GroupAddressBook(home, resource_name, resourceID, groupIDs)
+            
+            if not child:
+                child = cls(home, resource_name, resourceID, owned)
+            
+            metadata = items[3:]
+            for attr, value in zip(cls.metadataAttributes(), metadata):
+                setattr(child, attr, value)
+            child._syncTokenRevision = revisions[resourceID]
+            propstore = propertyStores.get(resourceID, None)
+            yield child._loadPropertyStore(propstore)
+            results.append(child)
+        returnValue(results)
+
+    @classmethod
+    def _homeChildLookup(cls, ownedPart):
+        """
+        Common portions of C{_ownedResourceIDByName}
+        C{_resourceIDSharedToHomeByName}, except for the 'owned' fragment of the
+        Where clause, supplied as an argument.
+        
+        ADDED return of bind.GROUP_BIND_ID for shared groups
+        """
+        bind = cls._bindSchema
+        return Select(
+            [bind.RESOURCE_ID, bind.GROUP_BIND_ID,],
+            From=bind,
+            Where=(bind.RESOURCE_NAME == Parameter('objectName')).And(
+                   bind.HOME_RESOURCE_ID == Parameter('homeID')).And(
+                    ownedPart))
+
+    @classproperty
+    def _resourceIDOwnedByHomeByName(cls): #@NoSelf
+        print("xxx AddressBook._resourceIDOwnedByHomeByName() cls=%s" % (cls,))
+        """
+        DAL query to look up an object resource ID owned by a home, given a
+        resource name (C{objectName}), and a home resource ID
+        (C{homeID}).
+        """
+        return cls._homeChildLookup(
+            cls._bindSchema.BIND_MODE == _BIND_MODE_OWN)
+
+
+    @classproperty
+    def _resourceIDSharedToHomeByName(cls): #@NoSelf
+        print("xxx AddressBook._resourceIDSharedToHomeByName() cls=%s" % (cls,))
+        """
+        DAL query to look up an object resource ID shared to a home, given a
+        resource name (C{objectName}), and a home resource ID
+        (C{homeID}).
+        """
+        return cls._homeChildLookup(
+            (cls._bindSchema.BIND_MODE != _BIND_MODE_OWN).And(
+                cls._bindSchema.BIND_STATUS == _BIND_STATUS_ACCEPTED))
+
+    @classmethod
+    @inlineCallbacks
+    def objectWithName(cls, home, name, owned):
+        print("xxx AddressBook.objectWithName() cls=%s, home=%s, name=%s, owned=%s" % (cls, home, name, owned,))
+        """
+        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.
+
+        @param owned: a boolean - whether or not to get a shared child
+        @return: an L{CommonHomeChild} or C{None} if no such child
+            exists.
+        """
+        data = None
+        queryCacher = home._txn.store().queryCacher
+        # Only caching non-shared objects so that we don't need to invalidate
+        # in sql_legacy
+        if owned and queryCacher:
+            # Retrieve data from cache
+            cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
+            data = yield queryCacher.get(cacheKey)
+
+        if data is None:
+            # No cached copy
+            if owned:
+                query = cls._resourceIDOwnedByHomeByName
+            else:
+                query = cls._resourceIDSharedToHomeByName
+            data = yield query.on(home._txn,
+                                  objectName=name, homeID=home._resourceID)
+            if owned and data and queryCacher:
+                # Cache the result
+                queryCacher.setAfterCommit(home._txn, cacheKey, data)
+
+        print("xxx AddressBook.objectWithName() data=%s name=%s" % (data, name,))
+        #traceback.print_exc()
+        
+        didGroupAddressbookQuery = False
+        if not data and owned:
+            data = yield cls._resourceIDSharedToHomeByName.on(home._txn,
+                                  objectName=name, homeID=home._resourceID)
+
+            print("xxx AddressBook.objectWithName() SHARED data=%s name=%s" % (data, name,))
+            didGroupAddressbookQuery = True
+            
+        if not data:
+            returnValue(None)
+
+        resourceID, groupBindID = data[0]
+        
+        bind = schema.GROUP_BIND
+        groupIDRows = (yield Select([bind.GROUP_ID,],
+             From=bind,
+             Where=(bind.ADDRESSBOOK_BIND_ID == groupBindID)).on(home._txn))
+        
+        print("xxx AddressBook.objectWithName() memberIDRows=%s name=%s" % (groupIDRows, name,))
+        
+        if groupIDRows:
+            child = GroupAddressBook(home, name, resourceID, groupIDRows[0][0])
+        elif didGroupAddressbookQuery:
+            child = None
+        else:
+            child = cls(home, name, resourceID, owned)
+            
+        if child:
+            yield child.initFromStore()
+        returnValue(child)
+
+
+
+
+class GroupAddressBook(AddressBook):
+    """
+    Implementation of L{IAddressBook} for shared group address books
+    """
+    implements(IAddressBook)
+
+
+    def __init__(self, home, name, resourceID, groupID):
+        """
+        Initialize an addressbook pointing at a path on disk.
+
+        @param name: the subdirectory of addressbookHome where this addressbook
+            resides.
+        @type name: C{str}
+
+        @param addressbookHome: the home containing this addressbook.
+        @type addressbookHome: L{AddressBookHome}
+
+        @param realName: If this addressbook was just created, the name which it
+        will eventually have on disk.
+        @type realName: C{str}
+        """
+
+        super(GroupAddressBook, self).__init__(home, name, resourceID, False)
+        print("xxx GroupAddressBook.__init__() self=%s, memberIDs=%s" % (self, groupID,))
+        self._objectResourceClass = GroupAddressBookObject
+
+        print("xxx GroupAddressBook.objectResources() self=%s, self._objectResourceClass=%s" % (self, self._objectResourceClass))
+
+        self._groupID = groupID
+
+
+    @classproperty
+    def _memberIDsForGroupIDQuery(cls): #@NoSelf
+        bind = schema.GROUP_MEMBERSHIP
+        return Select([bind.MEMBER_ID,],
+                      From=bind,
+                      Where=bind.GROUP_ID == Parameter("groupID"))
+
+
+
+
+    @classmethod
+    @inlineCallbacks
+    def listObjects(cls, home, owned):
+        print("xxx GroupAddressBook.listObjects() cls=%s" % (cls,))
+        """
+        Retrieve the names of the children that exist in the given home.
+
+        @return: an iterable of C{str}s.
+        """
+        # FIXME: tests don't cover this as directly as they should.
+        if owned:
+            rows = yield cls._ownedChildListQuery.on(
+                home._txn, resourceID=home._resourceID)
+        else:
+            rows = yield cls._sharedChildListQuery.on(
+                home._txn, resourceID=home._resourceID)
+        names = [row[0] for row in rows]
+        returnValue(names)
+
+
+    @classmethod
+    @inlineCallbacks
+    def objectWithName(cls, home, name, owned):
+        print("xxx GroupAddressBook.objectWithName() cls=%s" % (cls,))
+        """
+        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.
+
+        @param owned: a boolean - whether or not to get a shared child
+        @return: an L{CommonHomeChild} or C{None} if no such child
+            exists.
+        """
+        data = None
+        queryCacher = home._txn.store().queryCacher
+        # Only caching non-shared objects so that we don't need to invalidate
+        # in sql_legacy
+        if owned and queryCacher:
+            # Retrieve data from cache
+            cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
+            data = yield queryCacher.get(cacheKey)
+
+        if data is None:
+            # No cached copy
+            if owned:
+                query = cls._resourceIDOwnedByHomeByName
+            else:
+                query = cls._resourceIDSharedToHomeByName
+            data = yield query.on(home._txn,
+                                  objectName=name, homeID=home._resourceID)
+            if owned and data and queryCacher:
+                # Cache the result
+                queryCacher.setAfterCommit(home._txn, cacheKey, data)
+
+        if not data:
+            returnValue(None)
+
+        resourceID = data[0][0]
+        child = cls(home, name, resourceID, owned)
+        yield child.initFromStore()
+        returnValue(child)
+
+
+    @classmethod
+    @inlineCallbacks
+    def objectWithID(cls, home, resourceID):
+        print("xxx GroupAddressBook.objectWithID() cls=%s" % (cls,))
+        """
+        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.
+        """
+        data = yield cls._homeChildByIDQuery.on(
+            home._txn, resourceID=resourceID, homeID=home._resourceID)
+        if not data:
+            returnValue(None)
+        
+        # TODO: filter here
+            
+        name, mode = data[0]
+        child = cls(home, name, resourceID, mode == _BIND_MODE_OWN)
+        yield child.initFromStore()
+        returnValue(child)
+
+    
+    @classproperty
+    def _objectResourceNamesAndIDsQuery(cls): #@NoSelf
+        """
+        DAL query to load all object resource names for a home child.
+        """
+        obj = cls._objectSchema
+        return Select([obj.RESOURCE_NAME, obj.RESOURCE_ID], From=obj,
+                      Where=obj.PARENT_RESOURCE_ID == Parameter('resourceID'))
+
+    @inlineCallbacks
+    def allowedChildResourceIDs(self):
+        print("xxx GroupAddressBook.allowedChildResourceIDs() self=%s" % (self,))
+        groupMemberIDRows = yield self._memberIDsForGroupIDQuery.on(
+            self._txn, groupID=self._groupID)
+            
+        print("xxx GroupAddressBook.allowedChildResourceIDs(): groupMemberIDRows=%s" % (groupMemberIDRows,))
+        allowedChildResourceIDs = []
+        for groupMemberIDRow in groupMemberIDRows:
+            allowedChildResourceIDs += groupMemberIDRow
+        print("xxx GroupAddressBook.allowedChildResourceIDs(): allowedChildResourceIDs=%s" % (allowedChildResourceIDs,))
+        returnValue(allowedChildResourceIDs)
+
+
+    @inlineCallbacks
+    def listObjectResources(self):
+        print("xxx GroupAddressBook.listObjectResources() self=%s" % (self,))
+        if self._objectNames is None:
+        
+            # FIXME: this should be one query
+            rows = yield self._objectResourceNamesAndIDsQuery.on(
+                self._txn, resourceID=self._resourceID)
+            print("xxx GroupAddressBook.listObjectResources(): rows=%s" % (rows,))
+            
+            allowedChildResourceIDs = (yield self.allowedChildResourceIDs()) if rows else []
+            print("xxx GroupAddressBook.listObjectResources(): allowedChildResourceIDs=%s" % (allowedChildResourceIDs,))
+            
+            names = []
+            for row in rows:
+                print("xxx GroupAddressBook.listObjectResources(): row=%s" % (row,))
+                if row[1] in allowedChildResourceIDs:
+                    names += [row[0],]
+                
+            self._objectNames = sorted(names)
+            #self._objectNames = sorted([row[0] for row in rows])
+        print("xxx GroupAddressBook.listObjectResources(): self=%s returning=%s" % (self, self._objectNames,))
+        returnValue(self._objectNames)
+
+
+    @inlineCallbacks
+    def resourceNameForUID(self, uid):
+        print("xxx GroupAddressBook.resourceNameForUID() self=%s" % (self,))
+        try:
+            resource = self._objects[uid]
+            returnValue(resource.name() if resource else None)
+        except KeyError:
+            pass
+        rows = yield self._resourceNameForUIDQuery.on(
+            self._txn, uid=uid, resourceID=self._resourceID)
+        #FIXME: Filter
+
+        if rows:
+            returnValue(rows[0][0])
+        else:
+            self._objects[uid] = None
+            returnValue(None)
+
+    @inlineCallbacks
+    def resourceUIDForName(self, name):
+        print("xxx GroupAddressBook.resourceUIDForName() self=%s" % (self,))
+        try:
+            resource = self._objects[name]
+            returnValue(resource.uid() if resource else None)
+        except KeyError:
+            pass
+        rows = yield self._resourceUIDForNameQuery.on(
+            self._txn, name=name, resourceID=self._resourceID)
+        
+        #FIXME: Filter
+        if rows:
+            returnValue(rows[0][0])
+        else:
+            self._objects[name] = None
+            returnValue(None)
+
+ 
 class AddressBookObject(CommonObjectResource):
 
     implements(IAddressBookObject)
@@ -230,6 +669,7 @@
     def __init__(self, addressbook, name, uid, resourceID=None, metadata=None):
 
         super(AddressBookObject, self).__init__(addressbook, name, uid, resourceID)
+        self._invites = SQLLegacySharedGroupInvites(self)
 
 
     @property
@@ -339,4 +779,92 @@
         return MimeType.fromString("text/vcard; charset=utf-8")
 
 
+    @inlineCallbacks
+    def isSharedGroup(self):
+        """
+        FIXME:
+        How do we tell?
+        A shared address book is marked with a property.  Should we do the same with a group when it is shared?
+        
+        Otherwise, we can find the sharees address book bind table and see if there are any binds to this group.
+        But that requires the sharee context.
+        """
+        self.log_info("isSharedGroup(), self = %s, _resourceID=%s returing True by default" % (self, self._resourceID,))
+        
+        
+        
+        #need this for now, yuk!
+        (yield None)
+        returnValue(True)
 
+    def retrieveOldInvites(self):
+        return self._invites
+        
+
+class GroupAddressBookObject(AddressBookObject):
+    """
+    Override of AddressBookObject that filters children of GroupAddressBook
+    """
+
+    @classmethod
+    @inlineCallbacks
+    def loadAllObjects(cls, parent):
+        print("xxx GroupAddressBookObject.loadAllObjects() cls=%s, parent=%s" % (cls, parent))
+        """
+        Load all child objects and return 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 collection.
+        """
+
+        results = []
+
+        # Load from the main table first
+        dataRows = yield cls._allColumnsWithParent.on(
+            parent._txn, parentID=parent._resourceID)
+            
+
+        #filter
+        print("xxx GroupAddressBookObject.loadAllObjects(): dataRows=%s" % dataRows)
+        if dataRows and hasattr(parent, "allowedChildResourceIDs"):
+
+            allowedChildResourceIDs = (yield parent.allowedChildResourceIDs())
+            print("xxx GroupAddressBookObject.loadAllObjects(): allowedChildResourceIDs=%s" % allowedChildResourceIDs)
+            
+            filteredDataRows = []
+            for dataRow in dataRows:
+                if dataRow[0] in allowedChildResourceIDs:
+                    filteredDataRows += [dataRow,]
+            
+            dataRows = filteredDataRows
+            print("xxx GroupAddressBookObject.loadAllObjects(): filteredDataRows=%s" % dataRows)
+           
+            
+
+        if dataRows:
+        
+            # Get property stores for all these child resources (if any found)
+            if parent.objectResourcesHaveProperties():
+                propertyStores =(yield PropertyStore.forMultipleResources(
+                    parent._home.uid(),
+                    parent._txn,
+                    cls._objectSchema.RESOURCE_ID,
+                    cls._objectSchema.PARENT_RESOURCE_ID,
+                    parent._resourceID
+                ))
+            else:
+                propertyStores = {}
+
+        # Create the actual objects merging in properties
+        for row in dataRows:
+            child = cls(parent, "", None)
+            child._initFromRow(tuple(row))
+            yield child._loadPropertyStore(
+                props=propertyStores.get(child._resourceID, None)
+            )
+            results.append(child)
+
+        returnValue(results)
+
+
+

Modified: CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_legacy.py	2012-06-01 19:33:48 UTC (rev 9324)
+++ CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_legacy.py	2012-06-02 01:11:11 UTC (rev 9325)
@@ -450,7 +450,232 @@
         return self._txn.addressbookHomeWithUID(uid, create=True)
 
 
+class SQLLegacySharedGroupInvites(SQLLegacyAddressBookInvites):
+    """
+    Emulator for the implicit interface specified by
+    L{twistedcaldav.sharing.InvitesDatabase}.
+    """
 
+    @classproperty
+    def _childrenInAddressBookWithVCardUIDs(cls): #@NoSelf
+        bind = schema.ADDRESSBOOK_OBJECT
+        return Select([bind.RESOURCE_ID,],
+                      From=bind,
+                      Where=bind.ADDRESSBOOK_RESOURCE_ID == Parameter("addressbookID") and bind.VCARD_UID.In(Parameter("vcardUIDs")))
+
+    @classproperty
+    def _childInAddressBookWithVCardUID(cls): #@NoSelf
+        bind = schema.ADDRESSBOOK_OBJECT
+        return Select([bind.RESOURCE_ID,],
+                      From=bind,
+                      Where=(bind.ADDRESSBOOK_RESOURCE_ID == Parameter("addressbookID")).And(bind.VCARD_UID == Parameter("vcardUID")))
+        
+    @classproperty
+    def _memberIDsForGroupIDQuery(cls): #@NoSelf
+        bind = schema.GROUP_MEMBERSHIP
+        return Select([bind.GROUP_ID, bind.MEMBER_ID],
+                      From=bind,
+                      Where=bind.GROUP_ID == Parameter("groupID"))
+
+
+    @classproperty
+    def _insertGroupQuery(cls): #@NoSelf
+        bind = schema.GROUP_MEMBERSHIP
+        return Insert(
+            {
+                bind.GROUP_ID: Parameter("groupID"),
+                bind.MEMBER_ID: Parameter("memberID"),
+            }
+        )
+
+    @classproperty
+    def _deleteGroupQuery(cls): #@NoSelf
+        bind = schema.GROUP_MEMBERSHIP
+        return Delete(From=bind,
+                      Where=bind.MEMBER_ID == Parameter("memberID"))
+
+
+    @classproperty
+    def _insertGroupBindQuery(cls): #@NoSelf
+        bind = schema.GROUP_BIND
+        return Insert(
+            {
+                bind.ADDRESSBOOK_BIND_ID: Parameter("addressbookBindID"),
+                bind.GROUP_ID: Parameter("groupID"),
+            }
+        )
+        
+    @classproperty
+    def _insertBindQueryReturningBindID(cls): #@NoSelf
+        bind = cls._bindSchema
+        return Insert(
+            {
+                bind.HOME_RESOURCE_ID: Parameter("homeID"),
+                bind.RESOURCE_ID: Parameter("resourceID"),
+                bind.BIND_MODE: Parameter("mode"),
+                bind.BIND_STATUS: Parameter("status"),
+                bind.MESSAGE: Parameter("message"),
+                bind.RESOURCE_NAME: Parameter("resourceName"),
+                bind.SEEN_BY_OWNER: False,
+                bind.SEEN_BY_SHAREE: False,
+            },
+            Return=bind.GROUP_BIND_ID
+        )
+        
+    @inlineCallbacks
+    def updateMembership(self, group):
+       # first off, get a group list
+        @inlineCallbacks
+        def groupMemberUIDs(group):
+            vCard = (yield group.component())
+            memberUIDs = []
+            for memberProp in tuple(vCard.properties("X-ADDRESSBOOKSERVER-MEMBER")):
+                oneMember = memberProp.value()[len("urn:uuid:"):]
+                if len(oneMember):
+                    memberUIDs.append(oneMember)                       
+            returnValue(memberUIDs)
+                
+        print("updateMembership(): %s" % (group,))
+        memberUIDs = (yield groupMemberUIDs( group ))
+        print("updateMembership(): memberUIDs=%s" % (memberUIDs,))
+        
+        memberResourceIDs = []
+        for memberUID in memberUIDs:
+            
+            memberRow = (yield self._childInAddressBookWithVCardUID.on(
+                self._txn,
+                addressbookID=group._parentCollection._resourceID,
+                vcardUID=memberUID,
+            ))
+            print("updateMembership(): memberUID=%s, memberRow=%s" % (memberUID, memberRow, ))
+            if memberRow:
+                [[memberResourceID]] = memberRow
+                memberResourceIDs += [memberResourceID,]
+        
+        # expand groups here
+        
+        # add group resource id, and make set
+        memberResourceIDs += [group._resourceID,]
+        memberResourceIDs = set(memberResourceIDs)
+        print("updateMembership(): memberResourceIDs=%s" % (memberResourceIDs, ))
+       
+        # get current members
+        currentMemberRows = (yield self._memberIDsForGroupIDQuery.on(
+                self._txn,
+                groupID=group._resourceID,
+            ))
+        print("updateMembership(): currentMemberRows=%s" % (currentMemberRows, ))
+        
+        currentMemberResourceIDs = []
+        for row in tuple(currentMemberRows):
+            [groupID, memberID] = row
+            currentMemberResourceIDs += [memberID,]
+        
+        currentMemberResourceIDs = set(currentMemberResourceIDs)
+        print("updateMembership(): currentMemberResourceIDs=%s" % (currentMemberResourceIDs, ))
+       
+        membersToDelete = currentMemberResourceIDs.difference(memberResourceIDs)
+        print("updateMembership(): membersToDelete=%s" % (membersToDelete, ))
+        for memberToDelete in membersToDelete:
+            (yield self._deleteGroupQuery.on(
+            self._txn,
+            groupID=group._resourceID,
+            memberID=memberToDelete,
+            ))
+        
+        memberResourceIDsToAdd = memberResourceIDs.difference(currentMemberResourceIDs)
+        print("updateMembership(): memberResourceIDsToAdd=%s" % (memberResourceIDsToAdd, ))
+        for memberResourceIDToAdd in memberResourceIDsToAdd:
+            (yield self._insertGroupQuery.on(
+            self._txn,
+            groupID=group._resourceID,
+            memberID=memberResourceIDToAdd,
+            ))
+
+
+    @inlineCallbacks
+    def addOrUpdateRecord(self, record):
+        print("yyy addOrUpdateRecord() leg")
+        bindMode = {'read-only': _BIND_MODE_READ,
+                    'read-write': _BIND_MODE_WRITE}[record.access]
+        bindStatus = {
+            "NEEDS-ACTION": _BIND_STATUS_INVITED,
+            "ACCEPTED": _BIND_STATUS_ACCEPTED,
+            "DECLINED": _BIND_STATUS_DECLINED,
+            "INVALID": _BIND_STATUS_INVALID,
+        }[record.state]
+        shareeHome = yield self._getHomeWithUID(record.principalUID)
+        rows = yield self._idsForInviteUID.on(self._txn,
+                                              inviteuid=record.inviteuid)
+        
+        # FIXME: Do the BIND table query before the INVITE table query because BIND currently has proper
+        # constraints in place, whereas INVITE does not. Really we need to do this in a sub-transaction so
+        # we can roll back if any one query fails.
+        if rows:
+            print("xxx addOrUpdateRecord() leg rows=%s" % (rows,))
+            [[resourceID, homeResourceID]] = rows
+            yield self._updateBindQuery.on(
+                self._txn,
+                mode=bindMode, status=bindStatus, message=record.summary,
+                resourceID=resourceID, homeID=homeResourceID
+            )
+            yield self._updateInviteQuery.on(
+                self._txn, name=record.name, uid=record.inviteuid
+            )
+        else:
+            
+            #update membership
+            (yield self.updateMembership(self._collection))
+                
+                
+            
+                        
+            
+            #get members in address book
+            print("xxx addOrUpdateRecord(): self._collection = %s" % (self._collection,))
+             
+            print("xxx _insertBindQuery(): homeID = %s" % (shareeHome._resourceID,))
+            print("xxx _insertBindQuery(): resourceID = %s" % (self._collection._parentCollection._resourceID,))
+            print("xxx _insertBindQuery(): resourceName = %s" % (record.inviteuid,))
+            print("xxx _insertBindQuery(): mode = %s" % (bindMode,))
+            print("xxx _insertBindQuery(): status = %s" % (bindStatus,))
+            print("xxx _insertBindQuery(): message = %s" % (record.summary,))            
+            bindIDRow = (yield self._insertBindQueryReturningBindID.on(
+                self._txn,
+                homeID=shareeHome._resourceID,
+                resourceID=self._collection._parentCollection._resourceID,
+                resourceName=record.inviteuid,
+                mode=bindMode,
+                status=bindStatus,
+                message=record.summary
+            ))
+            
+            [[bindID]] = bindIDRow
+            print("xxx _insertGroupBindQuery(): groupID = %s" % (self._collection._resourceID,))
+            print("xxx _insertGroupBindQuery(): bindID = %s" % (bindID,))
+            yield self._insertGroupBindQuery.on(
+                self._txn,
+                groupID=self._collection._resourceID,
+                addressbookBindID=bindID,
+            )
+            print("xxx _insertInviteQuery(): uid = %s" % (record.inviteuid,))
+            print("xxx _insertInviteQuery(): name = %s" % (record.name,))
+            print("xxx _insertInviteQuery(): homeID = %s" % (shareeHome._resourceID,))
+            print("xxx _insertInviteQuery(): resourceID = %s" % (self._collection._parentCollection._resourceID,))
+            yield self._insertInviteQuery.on(
+                self._txn, uid=record.inviteuid, 
+                name=record.name,
+                homeID=shareeHome._resourceID,
+                resourceID=self._collection._parentCollection._resourceID,
+                recipient=record.userid
+            )
+        
+        # Must send notification to ensure cache invalidation occurs
+        #self._collection.notifyChanged()
+        self._collection._parentCollection.notifyChanged()
+
+
+
 class SQLLegacyShares(object):
 
     _homeTable = None

Modified: CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_schema/current.sql	2012-06-01 19:33:48 UTC (rev 9324)
+++ CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql_schema/current.sql	2012-06-02 01:11:11 UTC (rev 9325)
@@ -349,6 +349,17 @@
 );
 
 
+--------------------------
+-- AddressBook Metadata --
+--------------------------
+
+create table ADDRESSBOOK_METADATA (
+  RESOURCE_ID integer   primary key references ADDRESSBOOK on delete cascade, -- implicit index
+  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
 ----------------------
 -- AddressBook Bind --
 ----------------------
@@ -358,6 +369,7 @@
 create table ADDRESSBOOK_BIND (
   ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
   ADDRESSBOOK_RESOURCE_ID      integer      not null references ADDRESSBOOK on delete cascade,
+  GROUP_BIND_ID                integer      primary key default nextval('RESOURCE_ID_SEQ'),
 
   -- An invitation which hasn't been accepted yet will not yet have a resource
   -- name, so this field may be null.
@@ -369,7 +381,7 @@
   SEEN_BY_SHAREE               boolean      not null,
   MESSAGE                      text,                  -- FIXME: xml?
 
-  primary key(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID), -- implicit index
+  -- primary key(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID), -- implicit index
   unique(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
 );
 
@@ -391,27 +403,29 @@
 );
 
 --------------------------
--- Shared Group --
+-- Shared Group Members --
 --------------------------
 
-create table ADDRESSBOOK_GROUP_MEMBER (
+create table GROUP_MEMBERSHIP (
     GROUP_ID              integer      references ADDRESSBOOK_OBJECT, 
-    MEMBER                integer      references ADDRESSBOOK_OBJECT,
-    primary key(GROUP_ID, MEMBER) -- implicit index
+    MEMBER_ID             integer      references ADDRESSBOOK_OBJECT,
+    primary key(GROUP_ID, MEMBER_ID) -- implicit index
 );
 
---------------------------
--- AddressBook Metadata --
---------------------------
 
-create table ADDRESSBOOK_METADATA (
-  RESOURCE_ID   integer     primary key references ADDRESSBOOK on delete cascade, -- implicit index
-  GROUP_ID      integer     references ADDRESSBOOK_OBJECT, -- shared group
-  CREATED       timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
-  MODIFIED      timestamp   default timezone('UTC', CURRENT_TIMESTAMP)
+--------------------------------
+-- Shared Address Book Groups --
+--------------------------------
+
+create table GROUP_BIND (
+    ADDRESSBOOK_BIND_ID             integer      references ADDRESSBOOK_BIND, 
+    GROUP_ID integer,    -- temporary, should use line below when parser is fixed
+--  GROUP_ID integer      references GROUP_MEMBERSHIP(GROUP_ID),
+--  or add the following line
+--  foreign key(GROUP_ID) references GROUP_MEMBERSHIP(GROUP_ID)
+    primary key(ADDRESSBOOK_BIND_ID, GROUP_ID) -- implicit index
 );
 
-
 ---------------
 -- Revisions --
 ---------------
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120601/2c890411/attachment-0001.html>


More information about the calendarserver-changes mailing list