[CalendarServer-changes] [6654] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri Nov 19 13:21:05 PST 2010


Revision: 6654
          http://trac.macosforge.org/projects/calendarserver/changeset/6654
Author:   cdaboo at apple.com
Date:     2010-11-19 13:21:02 -0800 (Fri, 19 Nov 2010)
Log Message:
-----------
Make sure sharing invitations generate a push notification. Also clean up some stuff related to new store notification
classes including adding the depth:1 optimizations done in other classes.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/notify.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/datastore/sql.py

Modified: CalendarServer/trunk/twistedcaldav/notify.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/notify.py	2010-11-19 21:14:23 UTC (rev 6653)
+++ CalendarServer/trunk/twistedcaldav/notify.py	2010-11-19 21:21:02 UTC (rev 6654)
@@ -132,6 +132,9 @@
         newNotifier._prefix = self._prefix
         return newNotifier
 
+    def addID(self, label="default", id=None):
+        self._ids[label] = self.normalizeID(id)
+
     def getID(self, label="default"):
         id = self._ids.get(label, None)
         if self._prefix is None:

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2010-11-19 21:14:23 UTC (rev 6653)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2010-11-19 21:21:02 UTC (rev 6654)
@@ -1437,15 +1437,6 @@
         return True
 
 
-    @classmethod
-    def transform(cls, self, notifications, home):
-        """
-        Transform C{self} into a L{NotificationCollectionResource}.
-        """
-        self.__class__ = cls
-        self._initializeWithNotifications(notifications, home)
-
-
     @inlineCallbacks
     def makeChild(self, name):
         """
@@ -1531,74 +1522,6 @@
         )
 
 
-class StoreProtoNotificationCollectionResource(NotificationCollectionResource):
-    """
-    A resource representing a notification collection which hasn't yet been created.
-    """
-
-    def __init__(self, home, *args, **kw):
-        """
-        A placeholder resource for a notification collection which does not yet
-        exist, but will become a L{StoreNotificationCollectionResource}.
-
-        @param home: The calendar home which will be this resource's parent,
-            when it exists.
-
-        @type home: L{txdav.caldav.icalendarstore.ICalendarHome}
-        """
-        self._newStoreParentHome = home
-        super(StoreProtoNotificationCollectionResource, self).__init__(*args, **kw)
-
-
-    def isCollection(self):
-        return True
-
-    def makeChild(self, name):
-        # FIXME: this is necessary for 
-        # twistedcaldav.test.test_mkcalendar.
-        #     MKCALENDAR.test_make_calendar_no_parent - there should be a more
-        # structured way to refuse creation with a non-existent parent.
-        return NoParent()
-
-
-    def provisionFile(self):
-        """
-        Create a calendar collection.
-        """
-        # FIXME: there should be no need for this.
-        return self.createNotificationCollection()
-
-
-    def createNotificationCollection(self):
-        """
-        Override C{createCalendarCollection} to actually do the work.
-        """
-        d = succeed(CREATED)
-
-        notificationName = self.name()
-        self._newStoreParentHome.createChildWithName(notificationName)
-        newStoreNotification = self._newStoreParentHome.childWithName(
-            notificationName
-        )
-        StoreNotificationCollectionResource.transform(
-            self, newStoreNotification, self._newStoreParentHome
-        )
-        return d
-
-
-    def exists(self):
-        # FIXME: tests
-        return False
-
-
-    def provision(self):
-        """
-        This resource should do nothing if it's provisioned.
-        """
-        # FIXME: should be deleted, or raise an exception
-
-
-
 class StoreNotificationObjectFile(_NewStoreFileMetaDataHelper, NotificationResource):
     """
     A resource wrapping a calendar object.

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2010-11-19 21:14:23 UTC (rev 6653)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2010-11-19 21:21:02 UTC (rev 6654)
@@ -294,8 +294,80 @@
         abc = yield notifications.notificationObjectWithUID("abc")
         self.assertEquals((yield abc.xmldata()), inviteNotification2.toxml())
 
+    @inlineCallbacks
+    def test_addRemoveNotification(self):
+        """
+        L{INotificationCollection.writeNotificationObject} will silently
+        overwrite the notification object.
+        """
+        
+        # Prime the home collection first
+        yield self.transactionUnderTest().notificationsWithUID(
+            "home1"
+        )
+        yield self.commit()
 
+        notifications = yield self.transactionUnderTest().notificationsWithUID(
+            "home1"
+        )
+        self.notifierFactory.reset()
+        inviteNotification = InviteNotification()
+        yield notifications.writeNotificationObject("abc", inviteNotification,
+            inviteNotification.toxml())
+
+        yield self.commit()
+
+        # Make sure notification fired after commit
+        self.assertEquals(self.notifierFactory.history,
+            [
+                ("update", "CalDAV|home1"),
+                ("update", "CalDAV|home1/notification"),
+            ]
+        )
+
+        notifications = yield self.transactionUnderTest().notificationsWithUID(
+            "home1"
+        )
+        self.notifierFactory.reset()
+        yield notifications.removeNotificationObjectWithUID("abc")
+        abc = yield notifications.notificationObjectWithUID("abc")
+        self.assertEquals(abc, None)
+
+        yield self.commit()
+
+        # Make sure notification fired after commit
+        self.assertEquals(self.notifierFactory.history,
+            [
+                ("update", "CalDAV|home1"),
+                ("update", "CalDAV|home1/notification"),
+            ]
+        )
+
     @inlineCallbacks
+    def test_loadAllNotifications(self):
+        """
+        L{INotificationCollection.writeNotificationObject} will silently
+        overwrite the notification object.
+        """
+        notifications = yield self.transactionUnderTest().notificationsWithUID(
+            "home1"
+        )
+        inviteNotification = InviteNotification()
+        yield notifications.writeNotificationObject("abc", inviteNotification,
+            inviteNotification.toxml())
+        inviteNotification2 = InviteNotification(InviteSummary("a summary"))
+        yield notifications.writeNotificationObject(
+            "def", inviteNotification, inviteNotification2.toxml())
+
+        yield self.commit()
+
+        notifications = yield self.transactionUnderTest().notificationsWithUID(
+            "home1"
+        )
+        allObjects = yield notifications.notificationObjects()
+        self.assertEqual([obj.uid() for obj in allObjects], ["abc", "def"])
+
+    @inlineCallbacks
     def test_notificationObjectMetaData(self):
         """
         The objects retrieved from the notification home have various

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2010-11-19 21:14:23 UTC (rev 6653)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2010-11-19 21:21:02 UTC (rev 6654)
@@ -999,6 +999,8 @@
 
         # Update database
         self.retrieveOldIndex().addOrUpdateRecord(NotificationRecord(uid, name, xmltype.name))
+        
+        self.notifyChanged()
 
     @writeOperation
     def removeNotificationObjectWithName(self, name):
@@ -1016,9 +1018,16 @@
                 return lambda: None
             self._transaction.addOperation(do, "remove object resource object %r" %
                                            (name,))
+        
+            self.notifyChanged()
         else:
             raise NoSuchObjectResourceError(name)
 
+    @writeOperation
+    def removeNotificationObjectWithUID(self, uid):
+        name = uid + ".xml"
+        self.removeNotificationObjectWithName(name)
+
     def _doValidate(self, component):
         # Nothing to do - notifications are always generated internally by the server
         # so they better be valid all the time!
@@ -1032,8 +1041,8 @@
 
     def __init__(self, name, notifications):
         super(NotificationObject, self).__init__(name, notifications)
+        self._uid = name[:-4]
 
-
     def notificationCollection(self):
         return self._parentCollection
 
@@ -1124,8 +1133,6 @@
 
 
     def uid(self):
-        if not hasattr(self, "_uid"):
-            self._uid = self.xmldata
         return self._uid
 
     def initPropertyStore(self, props):

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2010-11-19 21:14:23 UTC (rev 6653)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2010-11-19 21:21:02 UTC (rev 6654)
@@ -144,6 +144,7 @@
         self._notifierFactory = notifierFactory
         self._label = label
         self._migrating = migrating
+        self._primaryHomeType = None
 
         CommonStoreTransaction.id += 1
         self._txid = CommonStoreTransaction.id
@@ -151,8 +152,11 @@
         extraInterfaces = []
         if enableCalendars:
             extraInterfaces.append(ICalendarTransaction)
+            self._primaryHomeType = ECALENDARTYPE
         if enableAddressBooks:
             extraInterfaces.append(IAddressBookTransaction)
+            if self._primaryHomeType is None:
+                self._primaryHomeType = EADDRESSBOOKTYPE
         directlyProvides(self, *extraInterfaces)
 
         from txdav.caldav.datastore.sql import CalendarHome
@@ -747,14 +751,17 @@
         results = []
 
         # Load from the main table first
+        if owned:
+            ownedPiece = "%(BIND:column_BIND_MODE)s = %%s"
+        else:
+            ownedPiece = "%(BIND:column_BIND_MODE)s != %%s and %(BIND:column_RESOURCE_NAME)s is not null"
         dataRows = (yield home._txn.execSQL(("""
             select %(CHILD:column_RESOURCE_ID)s, %(BIND:column_RESOURCE_NAME)s, %(CHILD:column_CREATED)s, %(CHILD:column_MODIFIED)s
             from %(CHILD:name)s
             left outer join %(BIND:name)s on (%(CHILD:column_RESOURCE_ID)s = %(BIND:column_RESOURCE_ID)s)
             where
-              %(BIND:column_HOME_RESOURCE_ID)s = %%s and
-              %(BIND:column_BIND_MODE)s """ + ("=" if owned else "!=") + """ %%s
-            """) % cls._homeChildBindTable,
+              %(BIND:column_HOME_RESOURCE_ID)s = %%s and """ + ownedPiece
+            ) % cls._homeChildBindTable,
             [
                 home._resourceID,
                 _BIND_MODE_OWN,
@@ -1637,7 +1644,6 @@
 
     compareAttributes = "_uid _resourceID".split()
 
-    _objectResourceClass = None
     _revisionsTable = NOTIFICATION_OBJECT_REVISIONS_TABLE
 
     def __init__(self, txn, uid, resourceID):
@@ -1646,7 +1652,23 @@
         self._uid = uid
         self._resourceID = resourceID
         self._notifications = {}
+        self._notificationNames = None
+        self._syncTokenRevision = None
 
+        # Make sure we have push notifications setup to push on this collection
+        # as well as the home it is in
+        if txn._notifierFactory:
+            childID = "%s/%s" % (uid, "notification")
+            notifier = txn._notifierFactory.newNotifier(
+                label="collection",
+                id=childID,
+                prefix=txn._homeClass[txn._primaryHomeType]._notifierPrefix
+            )
+            notifier.addID(id=uid)
+        else:
+            notifier = None
+        self._notifier = notifier
+
     @classmethod
     @inlineCallbacks
     def notificationsWithUID(cls, txn, uid):
@@ -1701,19 +1723,22 @@
 
     @inlineCallbacks
     def notificationObjects(self):
-        L = []
-        for name in (yield self.listNotificationObjects()):
-            L.append((yield self.notificationObjectWithName(name)))
-        returnValue(L)
+        results = (yield NotificationObject.loadAllObjects(self))
+        for result in results:
+            self._notifications[result.uid()] = result
+        self._notificationNames = sorted([result.name() for result in results])
+        returnValue(results)
 
 
     @inlineCallbacks
     def listNotificationObjects(self):
-        rows = yield self._txn.execSQL(
-            "select (NOTIFICATION_UID) from NOTIFICATION "
-            "where NOTIFICATION_HOME_RESOURCE_ID = %s",
-            [self._resourceID])
-        returnValue(sorted(["%s.xml" % row[0] for row in rows]))
+        if self._notificationNames is None:
+            rows = yield self._txn.execSQL(
+                "select (NOTIFICATION_UID) from NOTIFICATION "
+                "where NOTIFICATION_HOME_RESOURCE_ID = %s",
+                [self._resourceID])
+            self._notificationNames = sorted([row[0] for row in rows])
+        returnValue(self._notificationNames)
 
 
     def _nameToUID(self, name):
@@ -1770,35 +1795,29 @@
         yield self._deleteRevision("%s.xml" % (uid,))
 
 
+    @inlineCallbacks
     def _initSyncToken(self):
-        return self._txn.execSQL("""
+        self._syncTokenRevision = (yield self._txn.execSQL("""
             insert into %(name)s
             (%(column_HOME_RESOURCE_ID)s, %(column_RESOURCE_NAME)s, %(column_REVISION)s, %(column_DELETED)s)
             values (%%s, null, nextval('%(sequence)s'), FALSE)
+            returning %(column_REVISION)s
             """ % self._revisionsTable,
             [self._resourceID,]
-        )
+        ))[0][0]
 
 
     @inlineCallbacks
     def syncToken(self):
-        revision = (yield self._txn.execSQL(
-            """
-            select max(%(column_REVISION)s) from %(name)s
-            where %(column_HOME_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s is not null
-            """ % self._revisionsTable,
-            [self._resourceID,]
-        ))[0][0]
-
-        if revision is None:
-            revision = (yield self._txn.execSQL(
+        if self._syncTokenRevision is None:
+            self._syncTokenRevision = (yield self._txn.execSQL(
                 """
-                select %(column_REVISION)s from %(name)s
-                where %(column_HOME_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s is null
+                select max(%(column_REVISION)s) from %(name)s
+                where %(column_HOME_RESOURCE_ID)s = %%s
                 """ % self._revisionsTable,
                 [self._resourceID,]
             ))[0][0]
-        returnValue("%s#%s" % (self._resourceID, revision,))
+        returnValue("%s#%s" % (self._resourceID, self._syncTokenRevision,))
 
 
     def objectResourcesSinceToken(self, token):
@@ -1833,13 +1852,14 @@
 
 
     def _updateSyncToken(self):
-        return self._txn.execSQL("""
+        self._syncTokenRevision =  self._txn.execSQL("""
             update %(name)s
             set (%(column_REVISION)s) = (nextval('%(sequence)s'))
             where %(column_HOME_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s is null
+            returning %(column_REVISION)s
             """ % self._revisionsTable,
             [self._resourceID,]
-        )
+        )[0][0]
 
 
     def _insertRevision(self, name):
@@ -1857,27 +1877,24 @@
     @inlineCallbacks
     def _changeRevision(self, action, name):
 
-        nextrevision = yield self._txn.execSQL("""
-            select nextval('%(sequence)s')
-            """ % self._revisionsTable
-        )
-
         if action == "delete":
-            yield self._txn.execSQL("""
+            self._syncTokenRevision = (yield self._txn.execSQL("""
                 update %(name)s
-                set (%(column_REVISION)s, %(column_DELETED)s) = (%%s, TRUE)
+                set (%(column_REVISION)s, %(column_DELETED)s) = (nextval('%(sequence)s'), TRUE)
                 where %(column_HOME_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+                returning %(column_REVISION)s
                 """ % self._revisionsTable,
-                [nextrevision, self._resourceID, name]
-            )
+                [self._resourceID, name]
+            ))[0][0]
         elif action == "update":
-            yield self._txn.execSQL("""
+            self._syncTokenRevision = (yield self._txn.execSQL("""
                 update %(name)s
-                set (%(column_REVISION)s) = (%%s)
+                set (%(column_REVISION)s) = (nextval('%(sequence)s'))
                 where %(column_HOME_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+                returning %(column_REVISION)s
                 """ % self._revisionsTable,
-                [nextrevision, self._resourceID, name]
-            )
+                [self._resourceID, name]
+            ))[0][0]
         elif action == "insert":
             # Note that an "insert" may happen for a resource that previously existed and then
             # was deleted. In that case an entry in the REVISIONS table still exists so we have to
@@ -1890,28 +1907,46 @@
                 [self._resourceID, name, ]
             )))
             if found:
-                yield self._txn.execSQL("""
+                self._syncTokenRevision = (yield self._txn.execSQL("""
                     update %(name)s
-                    set (%(column_REVISION)s, %(column_DELETED)s) = (%%s, FALSE)
+                    set (%(column_REVISION)s, %(column_DELETED)s) = (nextval('%(sequence)s'), FALSE)
                     where %(column_HOME_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+                    returning %(column_REVISION)s
                     """ % self._revisionsTable,
-                    [nextrevision, self._resourceID, name]
-                )
+                    [self._resourceID, name]
+                ))[0][0]
             else:
-                yield self._txn.execSQL("""
+                self._syncTokenRevision = (yield self._txn.execSQL("""
                     insert into %(name)s
                     (%(column_HOME_RESOURCE_ID)s, %(column_RESOURCE_NAME)s, %(column_REVISION)s, %(column_DELETED)s)
-                    values (%%s, %%s, %%s, FALSE)
+                    values (%%s, %%s, nextval('%(sequence)s'), FALSE)
+                    returning %(column_REVISION)s
                     """ % self._revisionsTable,
-                    [self._resourceID, name, nextrevision]
-                )
+                    [self._resourceID, name,]
+                ))[0][0]
 
+        self.notifyChanged()
 
+
     def properties(self):
         return self._propertyStore
 
 
+    def notifierID(self, label="default"):
+        if self._notifier:
+            return self._notifier.getID(label)
+        else:
+            return None
 
+
+    def notifyChanged(self):
+        """
+        Trigger a notification of a change
+        """
+        if self._notifier:
+            self._txn.postCommit(self._notifier.notify)
+
+
 class NotificationObject(LoggingMixIn, FancyEqMixin):
 
     implements(INotificationObject)
@@ -1920,17 +1955,69 @@
 
     def __init__(self, home, uid):
         self._home = home
+        self._resourceID = None
         self._uid = uid
-        self._resourceID = None
         self._md5 = None
         self._size = None
         self._created = None
         self._modified = None
+        self._objectText = None
 
     def __repr__(self):
         return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
 
+    @classmethod
     @inlineCallbacks
+    def loadAllObjects(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 parent._txn.execSQL("""
+            select
+                RESOURCE_ID,
+                NOTIFICATION_UID,
+                MD5,
+                character_length(XML_DATA),
+                CREATED,
+                MODIFIED
+            from NOTIFICATION
+            where NOTIFICATION_HOME_RESOURCE_ID = %s
+            """,
+            [parent._resourceID]
+        ))
+        
+        if dataRows:
+            # Get property stores for all these child resources (if any found)
+            propertyStores =(yield PropertyStore.loadAll(
+                parent.uid(),
+                parent._txn,
+                "NOTIFICATION",
+                "NOTIFICATION.RESOURCE_ID",
+                "NOTIFICATION.NOTIFICATION_HOME_RESOURCE_ID",
+                parent._resourceID,
+            ))
+        
+        # Create the actual objects merging in properties
+        for row in dataRows:
+            child = cls(parent, None)
+            (child._resourceID,
+             child._uid,
+             child._md5,
+             child._size,
+             child._created,
+             child._modified,) = tuple(row)
+            yield child._loadPropertyStore(props=propertyStores.get(child._resourceID, None))
+            results.append(child)
+        
+        returnValue(results)
+
+    @inlineCallbacks
     def initFromStore(self):
         """
         Initialise this object from the store. We read in and cache all the extra metadata
@@ -1960,6 +2047,34 @@
         else:
             returnValue(None)
 
+    @inlineCallbacks
+    def _loadPropertyStore(self, props=None, created=False):
+        if props is None:
+            props = yield PropertyStore.load(
+                self._home.uid(),
+                self._txn,
+                self._resourceID,
+                created=created
+            )
+        self.initPropertyStore(props)
+        self._propertyStore = props
+
+
+    def properties(self):
+        return self._propertyStore
+
+
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+            ),
+            (
+                PropertyName.fromElement(NotificationType),
+            ),
+        )
+
+
     @property
     def _txn(self):
         return self._home._txn
@@ -2012,6 +2127,8 @@
             self._modified = rows[0][0]
 
         self.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
+        
+        self._objectText = xmldata
 
 
     @inlineCallbacks
@@ -2024,35 +2141,14 @@
         returnValue(data[0][0])
 
 
+    @inlineCallbacks
     def xmldata(self):
-        return self._fieldQuery("XML_DATA")
+        
+        if self._objectText is None:
+            self._objectText = (yield self._fieldQuery("XML_DATA"))
+        returnValue(self._objectText)
 
 
-    def properties(self):
-        return self._propertyStore
-
-
-    @inlineCallbacks
-    def _loadPropertyStore(self):
-        self._propertyStore = yield PropertyStore.load(
-            self._home.uid(),
-            self._txn,
-            self._resourceID
-        )
-        self.initPropertyStore(self._propertyStore)
-
-
-    def initPropertyStore(self, props):
-        # Setup peruser special properties
-        props.setSpecialProperties(
-            (
-            ),
-            (
-                PropertyName.fromElement(NotificationType),
-            ),
-        )
-
-
     def contentType(self):
         """
         The content type of NotificationObjects is text/xml.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20101119/c8509d9f/attachment-0001.html>


More information about the calendarserver-changes mailing list