[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