Revision: 9325 http://trac.macosforge.org/projects/calendarserver/changeset/9325 Author: gaya@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 -- ---------------