[CalendarServer-changes] [13786] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Jul 24 08:56:17 PDT 2014


Revision: 13786
          http://trac.calendarserver.org//changeset/13786
Author:   cdaboo at apple.com
Date:     2014-07-24 08:56:17 -0700 (Thu, 24 Jul 2014)
Log Message:
-----------
Don't update the TIME_RANGE table when there are no changes to it.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-07-24 15:55:09 UTC (rev 13785)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-07-24 15:56:17 UTC (rev 13786)
@@ -265,59 +265,59 @@
     #
     "BindAddresses": [], # List of IP addresses to bind to [empty = all]
     "BindHTTPPorts": [], # List of port numbers to bind to for HTTP
-                           # [empty = same as "Port"]
+                         # [empty = same as "Port"]
     "BindSSLPorts": [], # List of port numbers to bind to for SSL
-                           # [empty = same as "SSLPort"]
+                        # [empty = same as "SSLPort"]
     "InheritFDs": [], # File descriptors to inherit for HTTP requests
-                           # (empty = don't inherit)
+                      # (empty = don't inherit)
     "InheritSSLFDs": [], # File descriptors to inherit for HTTPS requests
-                           # (empty = don't inherit)
+                         # (empty = don't inherit)
     "MetaFD": 0, # Inherited file descriptor to call recvmsg() on to
-                           # receive sockets (none = don't inherit)
+                 # receive sockets (none = don't inherit)
 
     "UseMetaFD": True, # Use a 'meta' FD, i.e. an FD to transmit other FDs
-                           # to slave processes.
+                       # to slave processes.
 
     "UseDatabase": True, # True: database; False: files
 
     "TransactionTimeoutSeconds": 300, # Timeout transactions that take longer than
-                              # the specified number of seconds. Zero means
-                              # no timeouts. 5 minute default.
+                                      # the specified number of seconds. Zero means
+                                      # no timeouts. 5 minute default.
 
     "DBType": "", # 2 possible values: empty, meaning 'spawn postgres
-                           # yourself', or 'postgres', meaning 'connect to a
-                           # postgres database as specified by the 'DSN'
-                           # configuration key.  Will support more values in
-                           # the future.
+                  # yourself', or 'postgres', meaning 'connect to a
+                  # postgres database as specified by the 'DSN'
+                  # configuration key.  Will support more values in
+                  # the future.
 
     "SpawnedDBUser": "caldav", # The username to use when DBType is empty
 
     "DBImportFile": "", # File path to SQL file to import at startup (includes schema)
 
     "DSN": "", # Data Source Name.  Used to connect to an external
-                           # database if DBType is non-empty.  Format varies
-                           # depending on database type.
+               # database if DBType is non-empty.  Format varies
+               # depending on database type.
 
     "DBAMPFD": 0, # Internally used by database to tell slave
-                           # processes to inherit a file descriptor and use it
-                           # as an AMP connection over a UNIX socket; see
-                           # twext.enterprise.adbapi2.ConnectionPoolConnection
+                  # processes to inherit a file descriptor and use it
+                  # as an AMP connection over a UNIX socket; see
+                  # twext.enterprise.adbapi2.ConnectionPoolConnection
 
     "SharedConnectionPool": False, # Use a shared database connection pool in
-                                    # the master process, rather than having
-                                    # each client make its connections directly.
+                                   # the master process, rather than having
+                                   # each client make its connections directly.
 
     "FailIfUpgradeNeeded": True, # Set to True to prevent the server or utility
                                    # tools from running if the database needs a schema
                                    # upgrade.
     "StopAfterUpgradeTriggerFile": "stop_after_upgrade",   # if this file exists in ConfigRoot, stop
-                                                            # the service after finishing upgrade phase
+                                                           # the service after finishing upgrade phase
 
     "UpgradeHomePrefix": "",    # When upgrading, only upgrade homes where the owner UID starts with
-                                    # with the specified prefix. The upgrade will only be partial and only
-                                    # apply to upgrade pieces that affect entire homes. The upgrade will
-                                    # need to be run again without this prefix set to complete the overall
-                                    # upgrade.
+                                # with the specified prefix. The upgrade will only be partial and only
+                                # apply to upgrade pieces that affect entire homes. The upgrade will
+                                # need to be run again without this prefix set to complete the overall
+                                # upgrade.
 
     #
     # Work queue configuration information
@@ -652,21 +652,21 @@
     },
 
     "RestrictCalendarsToOneComponentType" : True, # Only allow calendars to be created with a single component type
-                                                   # If this is on, it will also trigger an upgrade behavior that will
-                                                   # split existing calendars into multiples based on component type.
-                                                   # If on, it will also cause new accounts to provision with separate
-                                                   # calendars for events and tasks.
+                                                  # If this is on, it will also trigger an upgrade behavior that will
+                                                  # split existing calendars into multiples based on component type.
+                                                  # If on, it will also cause new accounts to provision with separate
+                                                  # calendars for events and tasks.
 
     "SupportedComponents" : [                      # Set of supported iCalendar components
         "VEVENT",
         "VTODO",
-        #"VPOLL",
+        # "VPOLL",
     ],
 
     "ParallelUpgrades" : False, # Perform upgrades - currently only the
-                                   # database -> filesystem migration - but in
-                                   # the future, hopefully all relevant
-                                   # upgrades - in parallel in subprocesses.
+                                # database -> filesystem migration - but in
+                                # the future, hopefully all relevant
+                                # upgrades - in parallel in subprocesses.
 
     "MergeUpgrades": False, # During the upgrade phase of startup, rather than
                             # skipping homes found both on the filesystem and in
@@ -820,13 +820,13 @@
                 "Always"                          : False, # Override augments setting and always auto-schedule
                 "AllowUsers"                      : False, # Allow auto-schedule for users
                 "DefaultMode"                     : "automatic", # Default mode for auto-schedule processing, one of:
-                                                                   # "none"            - no auto-scheduling
-                                                                   # "accept-always"   - always accept, ignore busy time
-                                                                   # "decline-always"  - always decline, ignore free time
-                                                                   # "accept-if-free"  - accept if free, do nothing if busy
-                                                                   # "decline-if-busy" - decline if busy, do nothing if free
-                                                                   # "automatic"       - accept if free, decline if busy
-                "FutureFreeBusyDays"              : 3 * 365,       # How far into the future to check for booking conflicts
+                                                                 # "none"            - no auto-scheduling
+                                                                 # "accept-always"   - always accept, ignore busy time
+                                                                 # "decline-always"  - always decline, ignore free time
+                                                                 # "accept-if-free"  - accept if free, do nothing if busy
+                                                                 # "decline-if-busy" - decline if busy, do nothing if free
+                                                                 # "automatic"       - accept if free, decline if busy
+                "FutureFreeBusyDays"              : 3 * 365,     # How far into the future to check for booking conflicts
             },
 
             "WorkQueues" : {
@@ -1008,29 +1008,29 @@
                 "Port": 11311,
                 "HandleCacheTypes": [
                     "Default",
-#                   "OpenDirectoryBacker",
-#                   "ImplicitUIDLock",
-#                   "RefreshUIDLock",
-#                   "DIGESTCREDENTIALS",
-#                   "resourceInfoDB",
-#                   "pubsubnodes",
-#                   "FBCache",
-#                   "ScheduleAddressMapper",
-#                   "SQL.props",
-#                   "SQL.calhome",
-#                   "SQL.adbkhome",
+                    # "OpenDirectoryBacker",
+                    # "ImplicitUIDLock",
+                    # "RefreshUIDLock",
+                    # "DIGESTCREDENTIALS",
+                    # "resourceInfoDB",
+                    # "pubsubnodes",
+                    # "FBCache",
+                    # "ScheduleAddressMapper",
+                    # "SQL.props",
+                    # "SQL.calhome",
+                    # "SQL.adbkhome",
                 ]
             },
-#            "Shared": {
-#                "ClientEnabled": True,
-#                "ServerEnabled": True,
-#                "BindAddress": "127.0.0.1",
-#                "Port": 11211,
-#                "HandleCacheTypes": [
-#                    "ProxyDB",
-#                    "PrincipalToken",
-#                ]
-#            },
+            # "Shared": {
+            #     "ClientEnabled": True,
+            #     "ServerEnabled": True,
+            #     "BindAddress": "127.0.0.1",
+            #     "Port": 11211,
+            #     "HandleCacheTypes": [
+            #         "ProxyDB",
+            #         "PrincipalToken",
+            #     ]
+            # },
         },
         "memcached": "memcached", # Find in PATH
         "MaxMemory": 0, # Megabytes
@@ -1110,6 +1110,7 @@
     "FreeBusyIndexExpandAheadDays": 365,
     "FreeBusyIndexExpandMaxDays": 5 * 365,
     "FreeBusyIndexDelayedExpand": True,
+    "FreeBusyIndexSmartUpdate": True,
 
     # The RootResource uses a twext property store. Specify the class here
     "RootResourcePropStoreClass": "txweb2.dav.xattrprops.xattrPropertyStore",
@@ -1304,7 +1305,7 @@
                 # non-default values later.)  -glyph
                 if previousAbsoluteName in configDict and (
                         configDict[previousAbsoluteName] == inDict[lastPath]
-                    ):
+                ):
                     userSpecifiedPath = configDict[previousRelativeName]
                 else:
                     userSpecifiedPath = inDict[lastPath]
@@ -1359,12 +1360,16 @@
         maxConnections += configDict.MaxDBConnectionsPerPool
     else:
         # Otherwise the master *and* each worker process will be connecting
-        maxConnections += ((configDict.MultiProcess.ProcessCount + 1) *
-            configDict.MaxDBConnectionsPerPool)
+        maxConnections += (
+            (configDict.MultiProcess.ProcessCount + 1) *
+            configDict.MaxDBConnectionsPerPool
+        )
 
     configDict.Postgres.MaxConnections = maxConnections
-    configDict.Postgres.SharedBuffers = int(configDict.Postgres.MaxConnections *
-        configDict.Postgres.BuffersToConnectionsRatio)
+    configDict.Postgres.SharedBuffers = int(
+        configDict.Postgres.MaxConnections *
+        configDict.Postgres.BuffersToConnectionsRatio
+    )
 
 
 
@@ -1622,8 +1627,7 @@
                             topic = getAPNTopicFromCertificate(certPath)
                             service[protocol]["Topic"] = topic
                         else:
-                            log.error("APNS certificate not found: %s" %
-                                (certPath,))
+                            log.error("APNS certificate not found: %s" % (certPath,))
                     else:
                         log.error("APNS certificate path not specified")
 
@@ -1688,8 +1692,7 @@
                     pass
                 except KeychainPasswordNotFound:
                     # The password doesn't exist in the keychain.
-                    log.info("iMIP %s password not found in keychain" %
-                        (direction,))
+                    log.info("iMIP %s password not found in keychain" % (direction,))
 
 
 
@@ -1755,7 +1758,7 @@
     _preUpdateDirectoryService,
     _preUpdateResourceService,
     _preUpdateDirectoryAddressBookBackingDirectoryService,
-    )
+)
 POST_UPDATE_HOOKS = (
     _updateMultiProcess,
     _updateDataStore,
@@ -1773,7 +1776,7 @@
     _updateSharing,
     # _updateServers,
     _updateCompliance,
-    )
+)
 
 def _cleanup(configDict, defaultDict):
     cleanDict = copy.deepcopy(configDict)
@@ -1803,7 +1806,7 @@
         del cleanDict[oldKey]
 
     renamedOptions = {
-#       "BindAddress": "BindAddresses",
+        # "BindAddress": "BindAddresses",
     }
 
     for key in configDict:

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2014-07-24 15:55:09 UTC (rev 13785)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2014-07-24 15:56:17 UTC (rev 13786)
@@ -346,7 +346,7 @@
 
                     # If smart_merge is happening, then derive an instance in the new data as the change in the old
                     # data is valid and likely due to some other attendee changing their status.
-                    if  self.smart_merge:
+                    if self.smart_merge:
                         newOverride = self.newcalendar.deriveInstance(rid, allowCancelled=True)
                         if newOverride is None:
                             self._logDiffError("attendeeMerge: Could not derive instance for uncancelled component: %s" % (key,))
@@ -426,7 +426,7 @@
                 # We used to generate a 403 here - but instead we now ignore this error and let the server data
                 # override the client
                 self._logDiffError("attendeeMerge: Mismatched calendar objects")
-                #return False, False, (), None
+                # return False, False, (), None
             changeCausesReply |= reply
             if reply:
                 changedRids.append(rid)
@@ -646,8 +646,8 @@
             duration = component.getProperty("DURATION")
 
             timeRange = Period(
-                start=dtstart.value()  if dtstart  is not None else None,
-                end=dtend.value()    if dtend    is not None else None,
+                start=dtstart.value() if dtstart is not None else None,
+                end=dtend.value() if dtend is not None else None,
                 duration=duration.value() if duration is not None else None,
             )
             newdue = None
@@ -658,7 +658,7 @@
 
             if dtstart or duration:
                 timeRange = Period(
-                    start=dtstart.value()  if dtstart  is not None else None,
+                    start=dtstart.value() if dtstart is not None else None,
                     duration=duration.value() if duration is not None else None,
                 )
             else:
@@ -737,7 +737,7 @@
         return partstatChanged
 
 
-    def whatIsDifferent(self):
+    def whatIsDifferent(self, isiTip=True):
         """
         Compare the two calendar objects in their entirety and return a list of properties
         and PARTSTAT parameters that are different.
@@ -766,7 +766,7 @@
         for key in (oldset & newset):
             component1 = oldmap[key]
             component2 = newmap[key]
-            self._diffComponents(component1, component2, rids)
+            self._diffComponents(component1, component2, rids, isiTip)
 
         # Now verify that each additional component in oldset matches a derived component in newset
         for key in oldset - newset:
@@ -774,7 +774,7 @@
             newcomponent = self.newcalendar.deriveInstance(key[2])
             if newcomponent is None:
                 continue
-            self._diffComponents(oldcomponent, newcomponent, rids)
+            self._diffComponents(oldcomponent, newcomponent, rids, isiTip)
 
         # Now verify that each additional component in oldset matches a derived component in newset
         for key in newset - oldset:
@@ -782,11 +782,45 @@
             if oldcomponent is None:
                 continue
             newcomponent = newmap[key]
-            self._diffComponents(oldcomponent, newcomponent, rids)
+            self._diffComponents(oldcomponent, newcomponent, rids, isiTip)
 
         return rids
 
 
+    TRPROPS = frozenset((
+        "DTSTART",
+        "DTEND",
+        "DURATION",
+        "DUE",
+        "RECURRENCE-ID",
+        "RRULE",
+        "RDATE",
+        "EXDATE",
+        "STATUS",
+        "TRANSP",
+        "X-APPLE-TRAVEL-START",
+        "X-APPLE-TRAVEL-DURATION",
+        "X-APPLE-TRAVEL-RETURN",
+        "X-APPLE-TRAVEL-RETURN-DURATION",
+    ))
+
+    def timeRangeDifference(self):
+        """
+        Is there a difference between the two components that implies a change to the time or
+        transparency/status of any instance.
+
+        @return: L{True} if there is such a change, L{False} otherwise
+        @rtype: L{bool}
+        """
+
+        for props in self.whatIsDifferent(isiTip=False).values():
+            props = frozenset(props.keys())
+            if props & self.TRPROPS:
+                return True
+        else:
+            return False
+
+
     def attendeeNeedsAction(self, diffs):
         """
         Given a set of results from L{whatIsDifferent}, determine which recurrence-id's
@@ -856,20 +890,21 @@
         return (date_changed_rids, recurrence_reschedule,)
 
 
-    def _componentDuplicateAndNormalize(self, comp):
+    def _componentDuplicateAndNormalize(self, comp, isiTip=True):
         comp = comp.duplicate()
         comp.normalizePropertyValueLists("EXDATE")
-        comp.removePropertyParameters("ORGANIZER", ("SCHEDULE-STATUS",))
-        comp.removePropertyParameters("ATTENDEE", ("SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
-        comp.removePropertyParameters("VOTER", ("SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
         comp.removeAlarms()
         comp.normalizeAll()
         comp.normalizeAttachments()
-        iTipGenerator.prepareSchedulingMessage(comp, reply=True)
+        if isiTip:
+            comp.removePropertyParameters("ORGANIZER", ("SCHEDULE-STATUS",))
+            comp.removePropertyParameters("ATTENDEE", ("SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
+            comp.removePropertyParameters("VOTER", ("SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
+            iTipGenerator.prepareSchedulingMessage(comp, reply=True)
         return comp
 
 
-    def _diffComponents(self, comp1, comp2, rids):
+    def _diffComponents(self, comp1, comp2, rids, isiTip=True):
 
         assert isinstance(comp1, Component) and isinstance(comp2, Component)
 
@@ -878,8 +913,8 @@
             return
 
         # Duplicate then normalize for comparison
-        comp1 = self._componentDuplicateAndNormalize(comp1)
-        comp2 = self._componentDuplicateAndNormalize(comp2)
+        comp1 = self._componentDuplicateAndNormalize(comp1, isiTip)
+        comp2 = self._componentDuplicateAndNormalize(comp2, isiTip)
 
         # Diff all the properties
         propdiff = set(comp1.properties()) ^ set(comp2.properties())
@@ -888,7 +923,6 @@
         propsChanged = {}
         for prop in propdiff:
             if prop.name() in (
-                "TRANSP",
                 "DTSTAMP",
                 "CREATED",
                 "LAST-MODIFIED",

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2014-07-24 15:55:09 UTC (rev 13785)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2014-07-24 15:56:17 UTC (rev 13786)
@@ -164,7 +164,7 @@
         self.recipient_calendar_resource = None
         calendar_resource = (yield getCalendarObjectForRecord(self.txn, self.recipient.record, self.uid))
         if calendar_resource:
-            self.recipient_calendar = (yield calendar_resource.componentForUser(self.recipient.record.uid))
+            self.recipient_calendar = (yield calendar_resource.componentForUser(self.recipient.record.uid)).duplicate()
             self.recipient_calendar_resource = calendar_resource
 
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-07-24 15:55:09 UTC (rev 13785)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-07-24 15:56:17 UTC (rev 13786)
@@ -59,6 +59,7 @@
 from txdav.caldav.datastore.query.filter import Filter
 from txdav.caldav.datastore.query.generator import CalDAVSQLQueryGenerator
 from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.icaldiff import iCalDiff
 from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
 from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
 from txdav.caldav.datastore.scheduling.utils import uidFromCalendarUserAddress
@@ -1490,9 +1491,10 @@
         return Select(
             [co.RESOURCE_NAME],
             From=co,
-            Where=((co.RECURRANCE_MIN > Parameter("minDate"))
-                .Or(co.RECURRANCE_MAX < Parameter("maxDate")))
-                .And(co.CALENDAR_RESOURCE_ID == Parameter("resourceID"))
+            Where=(
+                (co.RECURRANCE_MIN > Parameter("minDate"))
+                .Or(co.RECURRANCE_MAX < Parameter("maxDate"))
+            ).And(co.CALENDAR_RESOURCE_ID == Parameter("resourceID"))
         )
 
 
@@ -1750,10 +1752,11 @@
             resourceID=child._resourceID
         )
 
+    # ===============================================================================
+    # Group sharing
+    # ===============================================================================
 
-    #===============================================================================
-    # Group sharing
-    #===============================================================================
+
     @inlineCallbacks
     def reconcileGroupSharee(self, groupUID):
         """
@@ -1775,9 +1778,9 @@
                         [bind.HOME_RESOURCE_ID],
                         From=bind,
                         Where=(bind.CALENDAR_RESOURCE_ID == self._resourceID).And(
-                            (bind.BIND_MODE == _BIND_MODE_GROUP).Or(
-                             bind.BIND_MODE == _BIND_MODE_GROUP_READ).Or(
-                             bind.BIND_MODE == _BIND_MODE_GROUP_WRITE)
+                            (bind.BIND_MODE == _BIND_MODE_GROUP)
+                            .Or(bind.BIND_MODE == _BIND_MODE_GROUP_READ)
+                            .Or(bind.BIND_MODE == _BIND_MODE_GROUP_WRITE)
                         )
                     )
                 )
@@ -1825,7 +1828,7 @@
             rows = yield Select(
                 [Count(gs.GROUP_ID)],
                 From=gs,
-                 Where=(
+                Where=(
                     gs.GROUP_ID.In(
                         Select(
                             [gm.GROUP_ID],
@@ -1871,7 +1874,7 @@
         rows = yield Select(
             [Count(gs.GROUP_ID)],
             From=gs,
-             Where=(
+            Where=(
                 gs.GROUP_ID.In(
                     Select(
                         [gm.GROUP_ID],
@@ -1957,7 +1960,7 @@
             rows = yield Select(
                 [Max(gs.GROUP_BIND_MODE)], # _BIND_MODE_WRITE > _BIND_MODE_READ
                 From=gs,
-                 Where=(
+                Where=(
                     gs.GROUP_ID.In(
                         Select(
                             [gm.GROUP_ID],
@@ -2224,7 +2227,7 @@
             "_created",
             "_modified",
             "_dataversion",
-         )
+        )
 
 
     @property
@@ -2358,22 +2361,19 @@
 
             if groupID in groupIDToMembershipHashMap:
                 if groupIDToMembershipHashMap[groupID] != membershipHash:
-                    yield Update({
-                            ga.MEMBERSHIP_HASH: membershipHash,
-                        },
+                    yield Update(
+                        {ga.MEMBERSHIP_HASH: membershipHash, },
                         Where=(ga.RESOURCE_ID == self._resourceID).And(
-                            ga.GROUP_ID == groupID
-                        )
+                            ga.GROUP_ID == groupID)
                     ).on(self._txn)
                     changed = True
                 del groupIDToMembershipHashMap[groupID]
             else:
                 yield Insert({
-                        ga.RESOURCE_ID: self._resourceID,
-                        ga.GROUP_ID: groupID,
-                        ga.MEMBERSHIP_HASH: membershipHash,
-                    }
-                ).on(self._txn)
+                    ga.RESOURCE_ID: self._resourceID,
+                    ga.GROUP_ID: groupID,
+                    ga.MEMBERSHIP_HASH: membershipHash,
+                }).on(self._txn)
                 changed = True
 
         if groupIDToMembershipHashMap:
@@ -2506,7 +2506,7 @@
                         "Attendee list size {0} is larger than allowed limit {1}".format(
                             attendeeListLength, config.MaxAttendeesPerInstance
                         )
-                )
+                    )
 
 
     @inlineCallbacks
@@ -2523,13 +2523,15 @@
             # Check for an allowed change
             if organizer is None and (
                 cutype == "ROOM" and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
-                cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
+                cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer
+            ):
                 raise ValidOrganizerError("Organizer required in calendar data for a {0}".format(cutype.lower(),))
 
             # Check for tracking the modifier
             if organizer is None and (
                 cutype == "ROOM" and config.Scheduling.Options.TrackUnscheduledLocationData or
-                cutype == "RESOURCE" and config.Scheduling.Options.TrackUnscheduledResourceData):
+                cutype == "RESOURCE" and config.Scheduling.Options.TrackUnscheduledResourceData
+            ):
 
                 # Find current principal and update modified by details
                 authz = yield self.directoryService().recordWithUID(self.calendar().viewerHome().authzuid().decode("utf-8"))
@@ -2588,7 +2590,7 @@
                 "X-CALENDARSERVER-ATTENDEE-COMMENT",
             ))
 
-            if old_has_private_comments and not new_has_private_comments:
+            if old_has_private_comments and not new_has_private_comments and internal_state == ComponentUpdateState.NORMAL:
                 # Transfer old comments to new calendar
                 log.debug("Organizer private comment properties were entirely removed by the client. Restoring existing properties.")
                 old_calendar = (yield self.componentForUser())
@@ -2813,12 +2815,16 @@
                                         "X-APPLE-RADIUS": "71",
                                         "X-TITLE": title,
                                     }
-                                    structured = Property("X-APPLE-STRUCTURED-LOCATION",
+                                    structured = Property(
+                                        "X-APPLE-STRUCTURED-LOCATION",
                                         geo.encode("utf-8"), params=params,
-                                        valuetype=Value.VALUETYPE_URI)
+                                        valuetype=Value.VALUETYPE_URI
+                                    )
                                     sub.replaceProperty(structured)
-                                    newLocProperty = Property("LOCATION",
-                                        "{0}\n{1}".format(title, street.encode("utf-8")))
+                                    newLocProperty = Property(
+                                        "LOCATION",
+                                        "{0}\n{1}".format(title, street.encode("utf-8"))
+                                    )
                                     sub.replaceProperty(newLocProperty)
 
 
@@ -3125,6 +3131,14 @@
                 if did_implicit_action:
                     self._componentChanged = True
 
+            if not hasattr(self, "tr_change"):
+                if inserting or hasattr(component, "noInstanceIndexing") or not config.FreeBusyIndexSmartUpdate:
+                    self.tr_change = None
+                else:
+                    oldcomponent = yield self.componentForUser()
+                    self.tr_change = iCalDiff(oldcomponent, component, False).timeRangeDifference()
+
+
             # Always do the per-user data merge right before we store
             component = (yield self.mergePerUserData(component, inserting))
 
@@ -3162,8 +3176,10 @@
             category = ChangeCategory.inbox
         elif internal_state == ComponentUpdateState.ORGANIZER_ITIP_UPDATE:
             category = ChangeCategory.organizerITIPUpdate
-        elif (internal_state == ComponentUpdateState.ATTENDEE_ITIP_UPDATE and
-            hasattr(self._txn, "doing_attendee_refresh")):
+        elif (
+            internal_state == ComponentUpdateState.ATTENDEE_ITIP_UPDATE and
+            hasattr(self._txn, "doing_attendee_refresh")
+        ):
             category = ChangeCategory.attendeeITIPUpdate
 
         yield self._calendar.notifyChanged(category=category)
@@ -3201,7 +3217,12 @@
         # In some cases there is no need to remove/rebuild the instance index because we know no time or
         # freebusy related properties have changed (e.g. an attendee reply and refresh). In those cases
         # the component will have a special attribute present to let us know to suppress the instance indexing.
-        instanceIndexingRequired = not getattr(component, "noInstanceIndexing", False) or inserting or reCreate
+        if inserting or reCreate:
+            instanceIndexingRequired = True
+        elif getattr(component, "noInstanceIndexing", False):
+            instanceIndexingRequired = False
+        else:
+            instanceIndexingRequired = getattr(self, "tr_change", True) in (None, True)
         instances = None
 
         if instanceIndexingRequired:
@@ -3276,7 +3297,7 @@
 
             # Now coerce indexing to off if needed
             if not doInstanceIndexing:
-                #instances = None # used by removeOldEventGroupLink() call at end
+                # instances = None # used by removeOldEventGroupLink() call at end
                 recurrenceLowerLimit = None
                 recurrenceLimit = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
 
@@ -3514,8 +3535,8 @@
                 self.log.error(
                     "Calendar data id={0} had unfixable problems:\n  {1}".format(
                         self._resourceID, "\n  ".join(unfixed),
-                        )
                     )
+                )
 
             if fixed:
                 self.log.error(
@@ -3695,8 +3716,7 @@
         txn = txn if txn is not None else self._txn
 
         rMin, rMax = (
-            yield self._recurrenceMinMaxByIDQuery.on(txn,
-                                         resourceID=self._resourceID)
+            yield self._recurrenceMinMaxByIDQuery.on(txn, resourceID=self._resourceID)
         )[0]
         returnValue((
             parseSQLDateToPyCalendar(rMin) if rMin is not None else None,
@@ -4535,7 +4555,7 @@
                 returnValue(None)
 
             # Check it is still split-able
-            will = (yield  cobj.willSplit())
+            will = (yield cobj.willSplit())
 
             if will:
                 # Now do the spitting
@@ -4606,8 +4626,7 @@
         # prevented from committing successfully.  It's not valid to have an
         # attachment that doesn't point to a real file.
 
-        home = (yield self._txn.calendarHomeWithResourceID(
-                    self._attachment._ownerHomeID))
+        home = (yield self._txn.calendarHomeWithResourceID(self._attachment._ownerHomeID))
 
         oldSize = self._attachment.size()
         newSize = self._file.tell()
@@ -4691,7 +4710,7 @@
         att = schema.ATTACHMENT
         if self._dropboxID:
             where = (att.DROPBOX_ID == self._dropboxID).And(
-                   att.PATH == self._name)
+                att.PATH == self._name)
         else:
             where = (att.ATTACHMENT_ID == self._attachmentID)
         rows = (yield Select(
@@ -5399,7 +5418,7 @@
         yield Delete(
             From=attco,
             Where=(attco.ATTACHMENT_ID == self._attachmentID).And(
-                   attco.CALENDAR_OBJECT_RESOURCE_ID == resourceID),
+                attco.CALENDAR_OBJECT_RESOURCE_ID == resourceID),
         ).on(self._txn)
 
         # References still exist - if not remove actual attachment

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2014-07-24 15:55:09 UTC (rev 13785)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2014-07-24 15:56:17 UTC (rev 13786)
@@ -50,7 +50,7 @@
 from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
 from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
 from txdav.caldav.datastore.scheduling.scheduler import ScheduleResponseQueue
-from txdav.caldav.datastore.sql import CalendarStoreFeatures
+from txdav.caldav.datastore.sql import CalendarStoreFeatures, CalendarObject
 from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource, \
     CommonStoreTransactionMonitor
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
@@ -192,8 +192,7 @@
         toHome = yield self.transactionUnderTest().calendarHomeWithUID(
             "new-home", create=True)
         toCalendar = yield toHome.calendarWithName("calendar")
-        ok, bad = (yield _migrateCalendar(fromCalendar, toCalendar,
-                               lambda x: x.component()))
+        ok, bad = (yield _migrateCalendar(fromCalendar, toCalendar, lambda x: x.component()))
         self.assertEqual(ok, 1)
         self.assertEqual(bad, 2)
 
@@ -212,8 +211,7 @@
         toHome = yield self.transactionUnderTest().calendarHomeWithUID(
             "new-home", create=True)
         toCalendar = yield toHome.calendarWithName("calendar")
-        ok, bad = (yield _migrateCalendar(fromCalendar, toCalendar,
-                               lambda x: x.component()))
+        ok, bad = (yield _migrateCalendar(fromCalendar, toCalendar, lambda x: x.component()))
         self.assertEqual(ok, 3)
         self.assertEqual(bad, 0)
 
@@ -400,8 +398,7 @@
         toHome = yield self.transactionUnderTest().calendarHomeWithUID(
             "home_attachments", create=True)
         toCalendar = yield toHome.calendarWithName("calendar")
-        ok, bad = (yield _migrateCalendar(fromCalendar, toCalendar,
-                               lambda x: x.component()))
+        ok, bad = (yield _migrateCalendar(fromCalendar, toCalendar, lambda x: x.component()))
         self.assertEqual(ok, 3)
         self.assertEqual(bad, 0)
 
@@ -421,14 +418,14 @@
                                lambda x: x.component())
 
         filter = caldavxml.Filter(
-                      caldavxml.ComponentFilter(
-                          caldavxml.ComponentFilter(
-                              caldavxml.TimeRange(start="%(now)s0201T000000Z" % self.nowYear, end="%(now)s0202T000000Z" % self.nowYear),
-                              name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
-                          ),
-                          name="VCALENDAR",
-                       )
-                  )
+            caldavxml.ComponentFilter(
+                caldavxml.ComponentFilter(
+                    caldavxml.TimeRange(start="%(now)s0201T000000Z" % self.nowYear, end="%(now)s0202T000000Z" % self.nowYear),
+                    name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+                ),
+                name="VCALENDAR",
+            )
+        )
         filter = Filter(filter)
         filter.settimezone(None)
 
@@ -640,8 +637,7 @@
 
         @inlineCallbacks
         def _defer1():
-            yield cal1.createObjectResourceWithName("1.ics", Component.fromString(
-"""BEGIN:VCALENDAR
+            yield cal1.createObjectResourceWithName("1.ics", Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 CALSCALE:GREGORIAN
@@ -686,8 +682,7 @@
 
         @inlineCallbacks
         def _defer2():
-            yield cal2.createObjectResourceWithName("2.ics", Component.fromString(
-"""BEGIN:VCALENDAR
+            yield cal2.createObjectResourceWithName("2.ics", Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 CALSCALE:GREGORIAN
@@ -768,7 +763,7 @@
         txn2 = calendarStore.newTransaction()
 
         notification_uid1_1 = yield txn1.notificationsWithUID(
-           "uid1",
+            "uid1",
         )
 
         @inlineCallbacks
@@ -813,9 +808,11 @@
         yield self.commit()
 
         prop = schema.RESOURCE_PROPERTY
-        _allWithID = Select([prop.NAME, prop.VIEWER_UID, prop.VALUE],
-                        From=prop,
-                        Where=prop.RESOURCE_ID == Parameter("resourceID"))
+        _allWithID = Select(
+            [prop.NAME, prop.VIEWER_UID, prop.VALUE],
+            From=prop,
+            Where=prop.RESOURCE_ID == Parameter("resourceID")
+        )
 
         # Check that one property is present
         home = yield self.homeUnderTest()
@@ -859,9 +856,11 @@
         resourceID = calobject._resourceID
 
         prop = schema.RESOURCE_PROPERTY
-        _allWithID = Select([prop.NAME, prop.VIEWER_UID, prop.VALUE],
-                        From=prop,
-                        Where=prop.RESOURCE_ID == Parameter("resourceID"))
+        _allWithID = Select(
+            [prop.NAME, prop.VIEWER_UID, prop.VALUE],
+            From=prop,
+            Where=prop.RESOURCE_ID == Parameter("resourceID")
+        )
 
         # No properties on existing calendar object
         rows = yield _allWithID.on(self.transactionUnderTest(), resourceID=resourceID)
@@ -912,9 +911,11 @@
         yield self.commit()
 
         prop = schema.RESOURCE_PROPERTY
-        _allWithID = Select([prop.NAME, prop.VIEWER_UID, prop.VALUE],
-                        From=prop,
-                        Where=prop.RESOURCE_ID == Parameter("resourceID"))
+        _allWithID = Select(
+            [prop.NAME, prop.VIEWER_UID, prop.VALUE],
+            From=prop,
+            Where=prop.RESOURCE_ID == Parameter("resourceID")
+        )
 
         # One property exists calendar object
         rows = yield _allWithID.on(self.transactionUnderTest(), resourceID=resourceID)
@@ -1502,6 +1503,7 @@
         # Re-add event with re-indexing
         calendar = yield self.calendarUnderTest()
         calendarObject = yield self.calendarObjectUnderTest(name="indexing.ics")
+        calendarObject.tr_change = True
         yield calendarObject.setComponent(component)
         instances2 = yield calendarObject.instances()
         self.assertNotEqual(
@@ -1933,8 +1935,10 @@
         rev = calendar._revisionsSchema
         yield Delete(
             From=rev,
-            Where=(rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
-                   rev.COLLECTION_NAME == Parameter("collectionName"))
+            Where=(
+                rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
+                rev.COLLECTION_NAME == Parameter("collectionName")
+            )
         ).on(self.transactionUnderTest(), homeID=home.id(), collectionName="calendar")
 
         yield self.commit()
@@ -2430,28 +2434,41 @@
 """.replace("\n", "\r\n")
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user01")
-        yield calendar.createCalendarObjectWithName("structured.ics",
-            Component.fromString(data))
-        cobj = yield self.calendarObjectUnderTest(name="structured.ics",
-            calendar_name="calendar", home="user01")
+        yield calendar.createCalendarObjectWithName(
+            "structured.ics",
+            Component.fromString(data)
+        )
+        cobj = yield self.calendarObjectUnderTest(
+            name="structured.ics",
+            calendar_name="calendar",
+            home="user01"
+        )
         comp = yield cobj.component()
         components = list(comp.subcomponents())
 
         # Check first component
         locProp = components[0].getProperty("LOCATION")
-        self.assertEquals(locProp.value(),
-            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014")
+        self.assertEquals(
+            locProp.value(),
+            "Room with Address 1\n1 Infinite Loop, Cupertino, CA 95014"
+        )
         structProp = components[0].getProperty("X-APPLE-STRUCTURED-LOCATION")
-        self.assertEquals(structProp.value(),
-            "geo:37.331741,-122.030333")
+        self.assertEquals(
+            structProp.value(),
+            "geo:37.331741,-122.030333"
+        )
 
         # Check second component
         locProp = components[1].getProperty("LOCATION")
-        self.assertEquals(locProp.value(),
-            "Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014")
+        self.assertEquals(
+            locProp.value(),
+            "Room with Address 2\n2 Infinite Loop, Cupertino, CA 95014"
+        )
         structProp = components[1].getProperty("X-APPLE-STRUCTURED-LOCATION")
-        self.assertEquals(structProp.value(),
-            "geo:37.332633,-122.030502")
+        self.assertEquals(
+            structProp.value(),
+            "geo:37.332633,-122.030502"
+        )
 
         yield self.commit()
 
@@ -2484,10 +2501,15 @@
         self.patch(config.HostedStatus, "Enabled", True)
 
         calendar = yield self.calendarUnderTest(name="calendar", home="user01")
-        yield calendar.createCalendarObjectWithName("external.ics",
-            Component.fromString(data))
-        cobj = yield self.calendarObjectUnderTest(name="external.ics",
-            calendar_name="calendar", home="user01")
+        yield calendar.createCalendarObjectWithName(
+            "external.ics",
+            Component.fromString(data)
+        )
+        cobj = yield self.calendarObjectUnderTest(
+            name="external.ics",
+            calendar_name="calendar",
+            home="user01"
+        )
         comp = yield cobj.component()
         components = list(comp.subcomponents())
 
@@ -6976,3 +6998,571 @@
         self.assertEqual(normalize_iCalStr(ical_future), normalize_iCalStr(data_future2) % relsubs, "Failed future: %s" % (title,))
         self.assertEqual(normalize_iCalStr(ical_past), normalize_iCalStr(data_past2) % relsubs, "Failed past: %s" % (title,))
         self.assertEqual(normalize_iCalStr(ical_inbox), normalize_iCalStr(data_inbox2) % relsubs, "Failed inbox: %s" % (title,))
+
+
+
+class TimeRangeUpdateOptimization(CommonCommonTests, unittest.TestCase):
+    """
+    CalendarObject splitting tests
+    """
+
+    EVENT1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event #2
+DTSTAMP:20100203T013909Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT3 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T130000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT4 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT5 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+STATUS:CANCELLED
+DTSTAMP:20100203T013909Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT6 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+X-APPLE-TRAVEL-DURATION;VALUE=DURATION:PT1H
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT7 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT8 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT9 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+RRULE:FREQ=DAILY
+EXDATE:{now}T120000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+    EVENT10 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+RRULE:FREQ=DAILY
+RDATE:{now}T150000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(TimeRangeUpdateOptimization, self).setUp()
+        yield self.buildStoreAndDirectory()
+        yield self.populate()
+
+        self.now = DateTime.getNowUTC()
+        self.now.setDateOnly(True)
+
+        self.trcount = 0
+        base_addInstances = CalendarObject._addInstances
+        def __addInstances(*args):
+            self.trcount += 1
+            return base_addInstances(*args)
+        self.patch(CalendarObject, "_addInstances", __addInstances)
+
+        self.patch(config, "FreeBusyIndexDelayedExpand", False)
+        self.patch(config, "FreeBusyIndexSmartUpdate", True)
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @property
+    def requirements(self):
+        return {
+            "home1": {
+                "calendar_1": {},
+            },
+            "user01": {
+                "calendar": {},
+                "inbox": {},
+            },
+            "user02": {
+                "calendar": {},
+                "inbox": {},
+            },
+        }
+
+
+    @inlineCallbacks
+    def test_initalPUT(self):
+        """
+        Test that initial PUT causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withoutTRChange(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT does not cause T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT2.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withoutOptimization(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        self.patch(config, "FreeBusyIndexSmartUpdate", False)
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT does cause T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT2.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withTRChange(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT causes T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT3.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withTranspChange(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT causes T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT4.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withStatusChange(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT causes T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT5.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withTravelTimeChange(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT causes T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT6.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withRRULEChange(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT causes T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT8.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withEXDATEAdd(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT causes T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT9.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    @inlineCallbacks
+    def test_updatePUT_withRDATEAdd(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest()
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.EVENT7.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 1)
+
+        # Second PUT causes T-R change
+        cobj = yield self.calendarObjectUnderTest()
+        yield cobj.setComponent(Component.fromString(self.EVENT10.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 2)
+
+
+    INVITE1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+    INVITE2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event
+DTSTAMP:20100203T013909Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+    INVITE3 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T120000Z
+DURATION:PT1H
+SUMMARY:New Event #2
+DTSTAMP:20100203T013909Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+    INVITE4 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100203T013849Z
+UID:uid1
+DTSTART:{now}T140000Z
+DURATION:PT1H
+SUMMARY:New Event #2
+DTSTAMP:20100203T013909Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+    @inlineCallbacks
+    def test_schedulingPUT(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        # Need schedule-q off for this test
+        self.patch(config.Scheduling.Options.WorkQueues, "Enabled", False)
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 3)
+
+        # Attendee reply does not cause T-R change (except for inbox item and attendee resource transp change)
+        cal = yield self.calendarUnderTest(home="user02", name="calendar")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        yield cobjs[0].setComponent(Component.fromString(self.INVITE2.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 5)
+
+        # Organizer summary change does not cause T-R change (except for inbox item)
+        cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
+        yield cobj.setComponent(Component.fromString(self.INVITE3.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 6)
+
+        # Organizer dtstart change causes T-R change
+        cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
+        yield cobj.setComponent(Component.fromString(self.INVITE4.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 9)
+
+
+    @inlineCallbacks
+    def test_schedulingPUT_withoutOptimization(self):
+        """
+        Test that second PUT withe time change causes a TIME_RANGE update
+        """
+
+        self.patch(config, "FreeBusyIndexSmartUpdate", False)
+
+        # Need schedule-q off for this test
+        self.patch(config.Scheduling.Options.WorkQueues, "Enabled", False)
+
+        # First PUT causes T-R change
+        cal = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield cal.createObjectResourceWithName("1.ics", Component.fromString(self.INVITE1.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 3)
+
+        # Attendee reply does cause T-R change (except for organizer update)
+        cal = yield self.calendarUnderTest(home="user02", name="calendar")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        yield cobjs[0].setComponent(Component.fromString(self.INVITE2.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 5)
+
+        # Organizer summary change causes T-R change
+        cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
+        yield cobj.setComponent(Component.fromString(self.INVITE3.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 8)
+
+        # Organizer dtstart change causes T-R change
+        cobj = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar")
+        yield cobj.setComponent(Component.fromString(self.INVITE4.format(now=self.now.getText())))
+        yield self.commit()
+
+        self.assertEqual(self.trcount, 11)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140724/853d458f/attachment-0001.html>


More information about the calendarserver-changes mailing list