[CalendarServer-changes] [8407] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Dec 6 12:36:21 PST 2011


Revision: 8407
          http://trac.macosforge.org/projects/calendarserver/changeset/8407
Author:   cdaboo at apple.com
Date:     2011-12-06 12:36:20 -0800 (Tue, 06 Dec 2011)
Log Message:
-----------
Make sure etag and getlastmodified work on collections. Required deferring etag() method, tweaking notifications to ensure modified
column in db is properly updated. Schema change applied to DB.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/push/applepush.py
    CalendarServer/trunk/calendarserver/webadmin/resource.py
    CalendarServer/trunk/calendarserver/webcal/resource.py
    CalendarServer/trunk/twext/web2/dav/resource.py
    CalendarServer/trunk/twext/web2/dav/static.py
    CalendarServer/trunk/twext/web2/static.py
    CalendarServer/trunk/twext/web2/test/test_static.py
    CalendarServer/trunk/twistedcaldav/directory/addressbook.py
    CalendarServer/trunk/twistedcaldav/directory/calendar.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/schedule.py
    CalendarServer/trunk/twistedcaldav/sharing.py
    CalendarServer/trunk/twistedcaldav/simpleresource.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/timezoneservice.py
    CalendarServer/trunk/twistedcaldav/timezonestdservice.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/carddav/datastore/sql.py
    CalendarServer/trunk/txdav/carddav/datastore/test/common.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_6_to_7.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_6_to_7.sql

Modified: CalendarServer/trunk/calendarserver/push/applepush.py
===================================================================
--- CalendarServer/trunk/calendarserver/push/applepush.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/calendarserver/push/applepush.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -26,7 +26,7 @@
 from twext.web2.server import parsePOSTData
 from twisted.application import service
 from twisted.internet import reactor, protocol
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twisted.internet.protocol import ClientFactory, ReconnectingClientFactory
 from twistedcaldav.extensions import DAVResource, DAVResourceWithoutChildrenMixin
 from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
@@ -498,7 +498,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def checkPreconditions(self, request):
         return None

Modified: CalendarServer/trunk/calendarserver/webadmin/resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/resource.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/calendarserver/webadmin/resource.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -504,7 +504,7 @@
 
     def etag(self):
         # Can't be calculated here
-        return None
+        return succeed(None)
 
     def contentLength(self):
         # Can't be calculated here

Modified: CalendarServer/trunk/calendarserver/webcal/resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webcal/resource.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/calendarserver/webcal/resource.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -38,7 +38,9 @@
 from twistedcaldav.config import config
 from twistedcaldav.extensions import DAVFile, ReadOnlyResourceMixIn
 
+from twisted.internet.defer import succeed
 
+
 class WebCalendarResource (ReadOnlyResourceMixIn, DAVFile):
 
     def defaultAccessControlList(self):
@@ -55,7 +57,7 @@
 
     def etag(self):
         # Can't be calculated here
-        return None
+        return succeed(None)
 
     def contentLength(self):
         # Can't be calculated here

Modified: CalendarServer/trunk/twext/web2/dav/resource.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/resource.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twext/web2/dav/resource.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twext.web2.dav.test.test_resource -*-
 ##
-# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
+# Copyright (c) 2005-2011 Apple Computer, Inc. All rights reserved.
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -231,7 +231,7 @@
                     returnValue(davxml.ResourceType.empty) #@UndefinedVariable
 
                 if name == "getetag":
-                    etag = self.etag()
+                    etag = (yield self.etag())
                     if etag is None:
                         returnValue(None)
                     returnValue(davxml.GETETag(etag.generate()))

Modified: CalendarServer/trunk/twext/web2/dav/static.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/static.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twext/web2/dav/static.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twext.web2.dav.test.test_static -*-
 ##
-# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
+# Copyright (c) 2005-2011 Apple Computer, Inc. All rights reserved.
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -86,9 +86,9 @@
     ##
 
     def etag(self):
-        if not self.fp.exists(): return None
+        if not self.fp.exists(): return succeed(None)
         if self.hasDeadProperty(TwistedGETContentMD5):
-            return http_headers.ETag(str(self.readDeadProperty(TwistedGETContentMD5)))
+            return succeed(http_headers.ETag(str(self.readDeadProperty(TwistedGETContentMD5))))
         else:
             return super(DAVFile, self).etag()
 

Modified: CalendarServer/trunk/twext/web2/static.py
===================================================================
--- CalendarServer/trunk/twext/web2/static.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twext/web2/static.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,7 +1,7 @@
 # -*- test-case-name: twext.web2.test.test_static -*-
 ##
 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
-# Copyright (c) 2010 Apple Computer, Inc. All rights reserved.
+# Copyright (c) 2010-2011 Apple Computer, Inc. All rights reserved.
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -39,7 +39,7 @@
 
 # Twisted Imports
 from twext.python.filepath import CachingFilePath as FilePath
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from zope.interface import implements
 
 class MetaDataMixin(object):
@@ -51,7 +51,7 @@
         """
         @return: The current etag for the resource if available, None otherwise.
         """
-        return None
+        return succeed(None)
 
     def lastModified(self):
         """
@@ -102,10 +102,11 @@
     def checkPreconditions(self, request):
         # This code replaces the code in resource.RenderMixin
         if request.method not in ("GET", "HEAD"):
+            etag = (yield self.etag())
             http.checkPreconditions(
                 request,
                 entityExists = self.exists(),
-                etag = self.etag(),
+                etag = etag,
                 lastModified = self.lastModified(),
             )
 
@@ -134,8 +135,9 @@
             # Content-* headers refer to the response content, not
             # (necessarily) to the resource content, so they depend on the
             # request method, and therefore can't be set here.
+            etag = (yield self.etag())
             for (header, value) in (
-                ("etag", self.etag()),
+                ("etag", etag),
                 ("last-modified", self.lastModified()),
             ):
                 if value is not None:
@@ -155,8 +157,8 @@
 
     def etag(self):
         lastModified = self.lastModified()
-        return http_headers.ETag("%X-%X" % (lastModified, hash(self.data)),
-                                 weak=(time.time() - lastModified <= 1))
+        return succeed(http_headers.ETag("%X-%X" % (lastModified, hash(self.data)),
+                                            weak=(time.time() - lastModified <= 1)))
 
     def lastModified(self):
         return self.creationDate()
@@ -247,7 +249,7 @@
         return self.fp.exists()
 
     def etag(self):
-        if not self.fp.exists(): return None
+        if not self.fp.exists(): return succeed(None)
 
         st = self.fp.statinfo
 
@@ -258,10 +260,10 @@
         #
         weak = (time.time() - st.st_mtime <= 1)
 
-        return http_headers.ETag(
+        return succeed(http_headers.ETag(
             "%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime),
             weak=weak
-        )
+        ))
 
     def lastModified(self):
         if self.fp.exists():

Modified: CalendarServer/trunk/twext/web2/test/test_static.py
===================================================================
--- CalendarServer/trunk/twext/web2/test/test_static.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twext/web2/test/test_static.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,4 +1,4 @@
-# Copyright (c) 2008 Twisted Matrix Laboratories.
+# Copyright (c) 2008-2011 Twisted Matrix Laboratories.
 # See LICENSE for details.
 
 """
@@ -34,7 +34,10 @@
         """
         Test that we can get an ETag
         """
-        self.failUnless(self.data.etag())
+        def _defer(result):
+            self.failUnless(result)
+        d = self.data.etag().addCallback(_defer)
+        return d
 
 
     def test_render(self):

Modified: CalendarServer/trunk/twistedcaldav/directory/addressbook.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/addressbook.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/directory/addressbook.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -31,7 +31,7 @@
 from twext.web2.http import HTTPError
 from twext.web2.http_headers import ETag, MimeType
 
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from twistedcaldav.config import config
 from twistedcaldav.directory.idirectory import IDirectoryService
@@ -66,7 +66,7 @@
         return config.ProvisioningResourceACL
 
     def etag(self):
-        return ETag(str(uuid4()))
+        return succeed(ETag(str(uuid4())))
 
     def contentType(self):
         return MimeType("httpd", "unix-directory")

Modified: CalendarServer/trunk/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendar.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/directory/calendar.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test.test_calendar -*-
 ##
-# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -66,7 +66,7 @@
         return config.ProvisioningResourceACL
 
     def etag(self):
-        return ETag(str(uuid4()))
+        return succeed(ETag(str(uuid4())))
 
     def contentType(self):
         return MimeType("httpd", "unix-directory")

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test.test_proxyprincipalmembers -*-
 ##
-# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -225,7 +225,7 @@
         return True
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def deadProperties(self):
         if not hasattr(self, "_dead_properties"):

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -163,7 +163,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def principalForShortName(self, recordType, name):
         return self.principalForRecord(self.directory.recordWithShortName(recordType, name))
@@ -756,7 +756,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     ##
     # HTTP

Modified: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.test.test_mail -*-
 ##
-# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -442,7 +442,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def checkPreconditions(self, request):
         return None

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -29,6 +29,7 @@
     "isAddressBookCollectionResource",
 ]
 
+import hashlib
 from urlparse import urlsplit
 import urllib
 import uuid
@@ -53,7 +54,7 @@
 from twext.web2.dav.resource import TwistedACLInheritable
 from twext.web2.dav.util import joinURL, parentForURL, normalizeURL
 from twext.web2.http import HTTPError, RedirectResponse, StatusResponse, Response
-from twext.web2.http_headers import MimeType
+from twext.web2.http_headers import MimeType, ETag
 from twext.web2.stream import MemoryStream
 
 from twistedcaldav import caldavxml, customxml
@@ -1479,6 +1480,7 @@
     # Stuff from CalDAVFile
     #
 
+    @inlineCallbacks
     def checkPreconditions(self, request):
         """
         We override the base class to handle the special implicit scheduling weak ETag behavior
@@ -1494,7 +1496,8 @@
                     if request.method not in ("GET", "HEAD"):
                         
                         # Always test against the current etag first just in case schedule-etags is out of sync
-                        etags = (self.etag(), ) + tuple([http_headers.ETag(etag) for etag in etags])
+                        etag = (yield self.etag())
+                        etags = (etag, ) + tuple([http_headers.ETag(etag) for etag in etags])
 
                         # Loop over each tag and succeed if any one matches, else re-raise last exception
                         exists = self.exists()
@@ -1519,13 +1522,12 @@
                     # Check per-method preconditions
                     method = getattr(self, "preconditions_" + request.method, None)
                     if method:
-                        response = maybeDeferred(method, request)
-                        response.addCallback(lambda _: request)
-                        return response
+                        returnValue((yield method(request)))
                     else:
-                        return None
+                        returnValue(None)
 
-        return super(CalDAVResource, self).checkPreconditions(request)
+        result = (yield super(CalDAVResource, self).checkPreconditions(request))
+        returnValue(result)
 
     @inlineCallbacks
     def createCalendar(self, request):
@@ -2483,11 +2485,28 @@
     def principalForRecord(self):
         raise NotImplementedError("Subclass must implement principalForRecord()")
 
+    @inlineCallbacks
+    def etag(self):
+        """
+        Use the sync token as the etag
+        """
+        if self._newStoreHome:
+            token = (yield self.getInternalSyncToken())
+            returnValue(ETag(hashlib.md5(token).hexdigest()))
+        else:
+            returnValue(None)
+
+    def lastModified(self):
+        return self._newStoreHome.modified() if self._newStoreHome else None
+
+    def creationDate(self):
+        return self._newStoreHome.created() if self._newStoreHome else None
+
     def notifierID(self, label="default"):
         self._newStoreHome.notifierID(label)
 
     def notifyChanged(self):
-        self._newStoreHome.notifyChanged()
+        return self._newStoreHome.notifyChanged()
 
     # Methods not supported
     http_ACL = None

Modified: CalendarServer/trunk/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/schedule.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/schedule.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test.test_calendar -*-
 ##
-# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -496,7 +496,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def checkPreconditions(self, request):
         return None

Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/sharing.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1046,7 +1046,7 @@
             yield sharedCollection.writeProperty(caldavxml.ScheduleCalendarTransp(caldavxml.Transparent()), request)
  
         # Notify client of changes
-        self.notifyChanged()
+        yield self.notifyChanged()
 
         # Return the URL of the shared collection
         returnValue(XMLResponse(
@@ -1092,7 +1092,7 @@
         yield self.sharesDB().removeRecordForShareUID(share.shareuid)
  
         # Notify client of changes
-        self.notifyChanged()
+        yield self.notifyChanged()
 
     @inlineCallbacks
     def declineShare(self, request, hostUrl, inviteUID):

Modified: CalendarServer/trunk/twistedcaldav/simpleresource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/simpleresource.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/simpleresource.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -71,7 +71,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
         return succeed(self.defaultACL)

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -59,10 +59,12 @@
 from txdav.base.propertystore.base import PropertyName
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.icommondatastore import NoSuchObjectResourceError
-from urlparse import urlsplit
-import time
 from txdav.idav import PropertyChangeNotAllowedError
 
+import time
+import hashlib
+from urlparse import urlsplit
+
 """
 Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
 L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
@@ -172,7 +174,7 @@
 
 
     def etag(self):
-        return ETag(self._newStoreObject.md5()) if self._newStoreObject is not None else None
+        return succeed(ETag(self._newStoreObject.md5()) if self._newStoreObject is not None else None)
 
 
     def contentType(self):
@@ -319,8 +321,16 @@
         return self._name
 
 
+    @inlineCallbacks
     def etag(self):
-        return ETag(self._newStoreObject.md5()) if self._newStoreObject else None
+        """
+        Use the sync token as the etag
+        """
+        if self._newStoreObject:
+            token = (yield self.getInternalSyncToken())
+            returnValue(ETag(hashlib.md5(token).hexdigest()))
+        else:
+            returnValue(None)
 
 
     def lastModified(self):
@@ -586,13 +596,14 @@
             
             if code is None:
                 
+                etag = (yield newchild.etag())
                 if not return_changed or dataChanged is None:
                     xmlresponses.append(
                         davxml.PropertyStatusResponse(
                             davxml.HRef.fromString(newchildURL),
                             davxml.PropertyStatus(
                                 davxml.PropertyContainer(
-                                    davxml.GETETag.fromString(newchild.etag().generate()),
+                                    davxml.GETETag.fromString(etag.generate()),
                                     customxml.UID.fromString(component.resourceUID()),
                                 ),
                                 davxml.Status.fromResponseCode(responsecode.OK),
@@ -605,7 +616,7 @@
                             davxml.HRef.fromString(newchildURL),
                             davxml.PropertyStatus(
                                 davxml.PropertyContainer(
-                                    davxml.GETETag.fromString(newchild.etag().generate()),
+                                    davxml.GETETag.fromString(etag.generate()),
                                     self.xmlDataElementType().fromTextData(dataChanged),
                                 ),
                                 davxml.Status.fromResponseCode(responsecode.OK),
@@ -757,12 +768,13 @@
             code = responsecode.BAD_REQUEST
         
         if code is None:
+            etag = (yield newchild.etag())
             xmlresponses.append(
                 davxml.PropertyStatusResponse(
                     davxml.HRef.fromString(newchildURL),
                     davxml.PropertyStatus(
                         davxml.PropertyContainer(
-                            davxml.GETETag.fromString(newchild.etag().generate()),
+                            davxml.GETETag.fromString(etag.generate()),
                             customxml.UID.fromString(component.resourceUID()),
                         ),
                         davxml.Status.fromResponseCode(responsecode.OK),
@@ -797,7 +809,8 @@
             yield updateResource.authorize(request, (davxml.Write(),))
 
             # Check if match
-            if ifmatch and ifmatch != updateResource.etag().generate():
+            etag = (yield updateResource.etag())
+            if ifmatch and ifmatch != etag.generate():
                 raise HTTPError(responsecode.PRECONDITION_FAILED)
             
             yield self.storeResourceData(request, updateResource, href, component, componentdata)
@@ -820,7 +833,7 @@
                     davxml.HRef.fromString(href),
                     davxml.PropertyStatus(
                         davxml.PropertyContainer(
-                            davxml.GETETag.fromString(updateResource.etag().generate()),
+                            davxml.GETETag.fromString(etag.generate()),
                         ),
                         davxml.Status.fromResponseCode(responsecode.OK),
                     )
@@ -850,7 +863,8 @@
                 raise HTTPError(responsecode.NOT_FOUND)
 
             # Check if match
-            if ifmatch and ifmatch != deleteResource.etag().generate():
+            etag = (yield deleteResource.etag())
+            if ifmatch and ifmatch != etag.generate():
                 raise HTTPError(responsecode.PRECONDITION_FAILED)
 
             yield deleteResource.storeRemove(
@@ -892,7 +906,7 @@
         self._newStoreObject.notifierID(label)
 
     def notifyChanged(self):
-        self._newStoreObject.notifyChanged()
+        return self._newStoreObject.notifyChanged()
 
 class _CalendarCollectionBehaviorMixin():
     """

Modified: CalendarServer/trunk/twistedcaldav/timezoneservice.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezoneservice.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/timezoneservice.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2011 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -72,7 +72,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def checkPreconditions(self, request):
         return None

Modified: CalendarServer/trunk/twistedcaldav/timezonestdservice.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezonestdservice.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/twistedcaldav/timezonestdservice.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -121,7 +121,7 @@
         return self._dead_properties
 
     def etag(self):
-        return None
+        return succeed(None)
 
     def checkPreconditions(self, request):
         return None

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -681,7 +681,7 @@
         else:
             yield self._calendar._updateRevision(self._name)
 
-        self._calendar.notifyChanged()
+        yield self._calendar.notifyChanged()
 
 
     @inlineCallbacks

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -921,12 +921,6 @@
             [
                 ("update", "CalDAV|home1"),
                 ("update", "CalDAV|home1/calendar_1"),
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_1"),
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_1"),
-                ("update", "CalDAV|home1"),
-                ("update", "CalDAV|home1/calendar_1"),
             ]
         )
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -351,7 +351,7 @@
     def test_putConcurrency(self):
         """
         Test that two concurrent attempts to PUT different calendar object
-        resources to the same address book home does not cause a deadlock.
+        resources to the same calendar home does not cause a deadlock.
         """
 
         calendarStore = self._sqlCalendarStore

Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -251,7 +251,7 @@
         else:
             yield self._addressbook._updateRevision(self._name)
 
-        self._addressbook.notifyChanged()
+        yield self._addressbook.notifyChanged()
 
 
     @inlineCallbacks

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/common.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/common.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -518,10 +518,6 @@
             [
                 ("update", "CardDAV|home1"),
                 ("update", "CardDAV|home1/addressbook_1"),
-                ("update", "CardDAV|home1"),
-                ("update", "CardDAV|home1/addressbook_1"),
-                ("update", "CardDAV|home1"),
-                ("update", "CardDAV|home1/addressbook_1"),
             ]
         )
 

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -199,6 +199,7 @@
         self._addressbookHomes = {}
         self._notificationHomes = {}
         self._notifierFactory = notifierFactory
+        self._notifiedAlready = set()
         self._migrating = migrating
 
         extraInterfaces = []
@@ -253,7 +254,13 @@
     def apnSubscriptionsByKey(self, key):
         return NotImplementedError
 
+    def isNotifiedAlready(self, obj):
+        return obj in self._notifiedAlready
+    
+    def notificationAddedForObject(self, obj):
+        self._notifiedAlready.add(obj)
 
+
 class StubResource(object):
     """
     Just enough resource to keep the shared sql DB classes going.
@@ -469,15 +476,10 @@
         self._transaction.addOperation(do, "create child %r" % (name,))
         props = c.properties()
         props[PropertyName(*ResourceType.qname())] = c.resourceType()
-        self.createdChild(c)
 
         self.notifyChanged()
         return c
 
-    def createdChild(self, child):
-        pass
-
-
     @writeOperation
     def removeChildWithName(self, name):
         if name.startswith(".") or name in self._removedChildren:
@@ -584,9 +586,12 @@
         """
         Trigger a notification of a change
         """
-        if self._notifiers:
+
+        # Only send one set of change notifications per transaction
+        if self._notifiers and not self._transaction.isNotifiedAlready(self):
             for notifier in self._notifiers:
                 self._transaction.postCommit(notifier.notify)
+            self._transaction.notificationAddedForObject(self)
 
 
 class CommonHomeChild(FileMetaDataMixin, LoggingMixIn, FancyEqMixin):
@@ -915,9 +920,12 @@
         """
         Trigger a notification of a change
         """
-        if self._notifiers:
+
+        # Only send one set of change notifications per transaction
+        if self._notifiers and not self._transaction.isNotifiedAlready(self):
             for notifier in self._notifiers:
                 self._transaction.postCommit(notifier.notify)
+            self._transaction.notificationAddedForObject(self)
 
 
 class CommonObjectResource(FileMetaDataMixin, LoggingMixIn, FancyEqMixin):

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2011-12-06 20:36:20 UTC (rev 8407)
@@ -62,7 +62,7 @@
 from twext.python.clsprop import classproperty
 from twext.enterprise.ienterprise import AlreadyFinishedError
 from twext.enterprise.dal.parseschema import significant
-from twext.enterprise.dal.syntax import Delete
+from twext.enterprise.dal.syntax import Delete, utcNowSQL
 from twext.enterprise.dal.syntax import Insert
 from twext.enterprise.dal.syntax import Len
 from twext.enterprise.dal.syntax import Max
@@ -234,6 +234,7 @@
         self._postCommitOperations = []
         self._postAbortOperations = []
         self._notifierFactory = notifierFactory
+        self._notifiedAlready = set()
         self._label = label
         self._migrating = migrating
         self._primaryHomeType = None
@@ -429,6 +430,12 @@
         self._postAbortOperations.append(operation)
 
 
+    def isNotifiedAlready(self, obj):
+        return obj in self._notifiedAlready
+    
+    def notificationAddedForObject(self, obj):
+        self._notifiedAlready.add(obj)
+
     _savepointCounter = 0
 
     def _savepoint(self):
@@ -713,6 +720,8 @@
         self._sharedChildren = {}
         self._notifiers = notifiers
         self._quotaUsedBytes = None
+        self._created = None
+        self._modified = None
 
         # Needed for REVISION/BIND table join
         self._revisionBindJoinTable = {}
@@ -739,6 +748,13 @@
                       From=home,
                       Where=home.RESOURCE_ID == Parameter("resourceID"))
 
+    @classproperty
+    def _metaDataQuery(cls): #@NoSelf
+        metadata = cls._homeMetaDataSchema
+        return Select([metadata.CREATED, metadata.MODIFIED],
+                      From=metadata,
+                      Where=metadata.RESOURCE_ID == Parameter("resourceID"))
+
     @inlineCallbacks
     def initFromStore(self, no_cache=False):
         """
@@ -755,6 +771,8 @@
 
         if result:
             self._resourceID = result[0][0]
+            self._created, self._modified = (yield self._metaDataQuery.on(
+                self._txn, resourceID=self._resourceID))[0]
             yield self._loadPropertyStore()
             returnValue(self)
         else:
@@ -956,10 +974,6 @@
         child = (yield self.childWithName(name))
         returnValue(child)
 
-    def createdChild(self, child):
-        pass
-
-
     @inlineCallbacks
     def removeChildWithName(self, name):
         child = yield self.childWithName(name)
@@ -1117,11 +1131,11 @@
 
 
     def created(self):
-        return None
+        return datetimeMktime(parseSQLTimestamp(self._created)) if self._created else None
 
 
     def modified(self):
-        return None
+        return datetimeMktime(parseSQLTimestamp(self._modified)) if self._modified else None
 
 
     @classproperty
@@ -1218,7 +1232,7 @@
                                            resourceID=self._resourceID)
             self._quotaUsedBytes = 0
 
-
+    
     def addNotifier(self, notifier):
         if self._notifiers is None:
             self._notifiers = ()
@@ -1242,13 +1256,45 @@
         else:
             returnValue(None)
 
+    @classproperty
+    def _changeLastModifiedQuery(cls): #@NoSelf
+        meta = cls._homeMetaDataSchema
+        return Update({meta.MODIFIED: utcNowSQL},
+                      Where=meta.RESOURCE_ID == Parameter("resourceID"),
+                      Return=meta.MODIFIED)
+
+    @inlineCallbacks
+    def bumpModified(self):
+        """
+        Bump the MODIFIED value. A possible deadlock could happen here if two or more
+        simultaneous changes are happening. In that case it is OK for the MODIFIED change
+        to fail so long as at least one works. We will use SAVEPOINT logic to handle
+        ignoring the deadlock error.
+        """
+
+        def _bumpModified(subtxn):
+            return self._changeLastModifiedQuery.on(subtxn, resourceID=self._resourceID)
+            
+        try:
+            self._modified = (yield self._txn.subtransaction(_bumpModified, retries=0))[0][0]
+        except AllRetriesFailed:
+            pass
+        
+    @inlineCallbacks
     def notifyChanged(self):
         """
         Trigger a notification of a change
         """
-        if self._notifiers:
+        
+        # Update modified if object still exists
+        if self._resourceID:
+            yield self.bumpModified()
+
+        # Only send one set of change notifications per transaction
+        if self._notifiers and not self._txn.isNotifiedAlready(self):
             for notifier in self._notifiers:
                 self._txn.postCommit(notifier.notify)
+            self._txn.notificationAddedForObject(self)
 
 
 
@@ -1903,10 +1949,9 @@
             PropertyName.fromElement(ResourceType)
         ] = child.resourceType()
         yield child._initSyncToken()
-        home.createdChild(child)
 
         # Change notification for a create is on the home collection
-        home.notifyChanged()
+        yield home.notifyChanged()
         returnValue(child)
 
 
@@ -1997,8 +2042,11 @@
         self._home._children[name] = self
         yield self._renameSyncToken()
 
-        self.notifyChanged()
+        yield self.notifyChanged()
 
+        # Make sure home collection modified is changed - not that we do not use _home.notifiedChanged() here
+        # since we are sending the notification on the existing child collection object
+        yield self._home.bumpModified()
 
 
     @classproperty
@@ -2023,9 +2071,13 @@
         self._modified   = None
         self._objects    = {}
 
-        self.notifyChanged()
+        yield self.notifyChanged()
 
+        # Make sure home collection modified is changed - not that we do not use _home.notifiedChanged() here
+        # since we are sending the notification on the previously existing child collection object
+        yield self._home.bumpModified()
 
+
     def ownerHome(self):
         return self._home
 
@@ -2247,7 +2299,7 @@
             self._objects.pop(name, None)
             self._objects.pop(uid, None)
             yield self._deleteRevision(name)
-            self.notifyChanged()
+            yield self.notifyChanged()
 
     @classproperty
     def _moveParentUpdateQuery(cls): #@NoSelf
@@ -2290,7 +2342,7 @@
         self._objects.pop(uid, None)
         self._objects.pop(child._resourceID, None)
         yield self._deleteRevision(name)
-        self.notifyChanged()
+        yield self.notifyChanged()
         
         # Adjust the child to be a child of the new parent and update ancillary tables
         yield self._moveParentUpdateQuery.on(
@@ -2303,7 +2355,7 @@
 
         # Signal sync change on new collection
         yield newparent._insertRevision(name)
-        newparent.notifyChanged()
+        yield newparent.notifyChanged()
 
     def objectResourcesHaveProperties(self):
         return False
@@ -2376,13 +2428,45 @@
         else:
             returnValue(None)
 
+    @classproperty
+    def _changeLastModifiedQuery(cls): #@NoSelf
+        schema = cls._homeChildSchema
+        return Update({schema.MODIFIED: utcNowSQL},
+                      Where=schema.RESOURCE_ID == Parameter("resourceID"),
+                      Return=schema.MODIFIED)
+
+    @inlineCallbacks
+    def bumpModified(self):
+        """
+        Bump the MODIFIED value. A possible deadlock could happen here if two or more
+        simultaneous changes are happening. In that case it is OK for the MODIFIED change
+        to fail so long as at least one works. We will use SAVEPOINT logic to handle
+        ignoring the deadlock error.
+        """
+
+        def _bumpModified(subtxn):
+            return self._changeLastModifiedQuery.on(subtxn, resourceID=self._resourceID)
+            
+        try:
+            self._modified = (yield self._txn.subtransaction(_bumpModified, retries=0))[0][0]
+        except AllRetriesFailed:
+            pass
+        
+    @inlineCallbacks
     def notifyChanged(self):
         """
         Trigger a notification of a change
         """
-        if self._notifiers:
+        
+        # Update modified if object still exists
+        if self._resourceID:
+            yield self.bumpModified()
+
+        # Only send one set of change notifications per transaction
+        if self._notifiers and not self._txn.isNotifiedAlready(self):
             for notifier in self._notifiers:
                 self._txn.postCommit(notifier.notify)
+            self._txn.notificationAddedForObject(self)
 
 
 
@@ -2988,9 +3072,12 @@
         """
         Trigger a notification of a change
         """
-        if self._notifiers:
+
+        # Only send one set of change notifications per transaction
+        if self._notifiers and not self._txn.isNotifiedAlready(self):
             for notifier in self._notifiers:
                 self._txn.postCommit(notifier.notify)
+            self._txn.notificationAddedForObject(self)
 
 
     @classproperty

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2011-12-06 20:36:20 UTC (rev 8407)
@@ -39,7 +39,9 @@
 
 create table CALENDAR_HOME_METADATA (
   RESOURCE_ID      integer      primary key references CALENDAR_HOME on delete cascade, -- implicit index
-  QUOTA_USED_BYTES integer      default 0 not null
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
 );
 
 --------------
@@ -324,7 +326,9 @@
 
 create table ADDRESSBOOK_HOME_METADATA (
   RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
-  QUOTA_USED_BYTES integer      default 0 not null
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
 );
 
 -----------------

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_6_to_7.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_6_to_7.sql	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_6_to_7.sql	2011-12-06 20:36:20 UTC (rev 8407)
@@ -22,6 +22,12 @@
 alter table CALENDAR_HOME
  add ("DATAVERSION" integer default 1 null);
  
+-- Need to add timestamp columns
+alter table CALENDAR_HOME_METADATA
+ add ("CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC');
+alter table CALENDAR_HOME_METADATA
+ add ("MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC');
+
  -- Just need to modify one column
 alter table CALENDAR_OBJECT
  add ("SUPPORTED_COMPONENTS" nvarchar2(255) default null);
@@ -30,6 +36,12 @@
 alter table ADDRESSBOOK_HOME
  add ("DATAVERSION" integer default 1 null);
  
+-- Need to add timestamp columns
+alter table ADDRESSBOOK_HOME_METADATA
+ add ("CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC');
+alter table ADDRESSBOOK_HOME_METADATA
+ add ("MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC');
+
 -- Now update the version
 update CALENDARSERVER set VALUE = '7' where NAME = 'VERSION';
 

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_6_to_7.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_6_to_7.sql	2011-12-06 20:16:33 UTC (rev 8406)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_6_to_7.sql	2011-12-06 20:36:20 UTC (rev 8407)
@@ -22,14 +22,24 @@
 alter table CALENDAR_HOME
  add column DATAVERSION integer default 1 null;
  
+-- Need to add timestamp columns
+alter table CALENDAR_HOME_METADATA
+ add column CREATED  timestamp  default timezone('UTC', CURRENT_TIMESTAMP),
+ add column MODIFIED timestamp  default timezone('UTC', CURRENT_TIMESTAMP);
+ 
 -- Just need to add one column
 alter table CALENDAR
- add column SUPPORTED_COMPONENTS        varchar(255) default null;
+ add column SUPPORTED_COMPONENTS  varchar(255) default null;
 
 -- Just need to add one column
 alter table ADDRESSBOOK_HOME
  add column DATAVERSION integer default 1 null;
  
+-- Need to add timestamp columns
+alter table ADDRESSBOOK_HOME_METADATA
+ add column CREATED  timestamp  default timezone('UTC', CURRENT_TIMESTAMP),
+ add column MODIFIED timestamp  default timezone('UTC', CURRENT_TIMESTAMP);
+ 
 -- Now update the version
 update CALENDARSERVER set VALUE = '7' where NAME = 'VERSION';
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20111206/25702e61/attachment-0001.html>


More information about the calendarserver-changes mailing list