[CalendarServer-changes] [11026] CalendarServer/branches/users/cdaboo/store-scheduling/txdav

source_changes at macosforge.org source_changes at macosforge.org
Wed Apr 10 14:43:39 PDT 2013


Revision: 11026
          http://trac.calendarserver.org//changeset/11026
Author:   cdaboo at apple.com
Date:     2013-04-10 14:43:39 -0700 (Wed, 10 Apr 2013)
Log Message:
-----------
Checkpoint: most of put_common now in the store - next up is to hook up implicit scheduling.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py
    CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-10 21:28:48 UTC (rev 11025)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/sql.py	2013-04-10 21:43:39 UTC (rev 11026)
@@ -14,7 +14,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+
 from twext.enterprise.locking import NamedLock
+from urlparse import urlparse, urlunparse
+from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
 
 """
 SQL backend for CalDAV storage.
@@ -66,7 +69,8 @@
     IAttachment, AttachmentStoreFailed, AttachmentStoreValidManagedID, \
     AttachmentMigrationFailed, AttachmentDropboxNotAllowed, \
     TooManyAttendeesError, InvalidComponentTypeError, InvalidCalendarAccessError, \
-    InvalidUIDError, UIDExistsError
+    InvalidUIDError, UIDExistsError, ResourceDeletedError, \
+    AttendeeAllowedError, InvalidPerUserDataMerge, ComponentUpdateState
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE
@@ -773,6 +777,52 @@
         return calendar._resourceID in (self._default_events, self._default_tasks)
 
 
+    def getDefaultAlarm(self, vevent, timed):
+        """
+        Return the default alarm (text) for the specified alarm type.
+
+        @param vevent: used for a vevent (C{True}) or vtodo (C{False})
+        @type vevent: C{bool}
+        @param timed: timed ({C{True}) or all-day ({C{False})
+        @type timed: C{bool}
+        @return: the alarm (text)
+        @rtype: C{str}
+        """
+
+        if vevent:
+            propname = caldavxml.DefaultAlarmVEventDateTime if timed else caldavxml.DefaultAlarmVEventDate
+        else:
+            propname = caldavxml.DefaultAlarmVToDoDateTime if timed else caldavxml.DefaultAlarmVToDoDate
+
+        prop = self.properties().get(PropertyName.fromElement(propname))
+
+        return str(prop) if prop is not None else None
+
+
+    def setDefaultAlarm(self, alarm, vevent, timed):
+        """
+        Set default alarm of the specified type.
+
+        @param alarm: the alarm text
+        @type alarm: C{str}
+        @param vevent: used for a vevent (C{True}) or vtodo (C{False})
+        @type vevent: C{bool}
+        @param timed: timed ({C{True}) or all-day ({C{False})
+        @type timed: C{bool}
+        """
+
+        if vevent:
+            prop = caldavxml.DefaultAlarmVEventDateTime if timed else caldavxml.DefaultAlarmVEventDate
+        else:
+            prop = caldavxml.DefaultAlarmVToDoDateTime if timed else caldavxml.DefaultAlarmVToDoDate
+
+        self.properties()[PropertyName.fromElement(prop)] = prop.fromString(alarm)
+
+
+    def principal(self):
+        return self.principalForUID(self.uid())
+
+
     def principalForUID(self, uid):
         return CalendarPrincipal(uid, ("urn:uuid:%s" % (uid,), "mailto:%s at example.com" % (uid,),))
 
@@ -912,13 +962,9 @@
             Where=(cal.RESOURCE_ID == self._resourceID)
         ).on(self._txn)
         self._supportedComponents = supported_components
+        yield self.invalidateQueryCache()
 
-        queryCacher = self._txn._queryCacher
-        if queryCacher is not None:
-            cacheKey = queryCacher.keyForHomeChildMetaData(self._resourceID)
-            yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
 
-
     def getSupportedComponents(self):
         return self._supportedComponents
 
@@ -930,6 +976,51 @@
             return True
 
 
+    def getDefaultAlarm(self, vevent, timed):
+        """
+        Return the default alarm (text) for the specified alarm type.
+
+        @param vevent: used for a vevent (C{True}) or vtodo (C{False})
+        @type vevent: C{bool}
+        @param timed: timed ({C{True}) or all-day ({C{False})
+        @type timed: C{bool}
+        @return: the alarm (text)
+        @rtype: C{str}
+        """
+
+        if vevent:
+            propname = caldavxml.DefaultAlarmVEventDateTime if timed else caldavxml.DefaultAlarmVEventDate
+        else:
+            propname = caldavxml.DefaultAlarmVToDoDateTime if timed else caldavxml.DefaultAlarmVToDoDate
+
+        prop = self.properties().get(PropertyName.fromElement(propname))
+
+        if prop is None:
+            return self.viewerHome().getDefaultAlarm(vevent, timed)
+        else:
+            return str(prop)
+
+
+    def setDefaultAlarm(self, alarm, vevent, timed):
+        """
+        Set default alarm of the specified type.
+
+        @param alarm: the alarm text
+        @type alarm: C{str}
+        @param vevent: used for a vevent (C{True}) or vtodo (C{False})
+        @type vevent: C{bool}
+        @param timed: timed ({C{True}) or all-day ({C{False})
+        @type timed: C{bool}
+        """
+
+        if vevent:
+            prop = caldavxml.DefaultAlarmVEventDateTime if timed else caldavxml.DefaultAlarmVEventDate
+        else:
+            prop = caldavxml.DefaultAlarmVToDoDateTime if timed else caldavxml.DefaultAlarmVToDoDate
+
+        self.properties()[PropertyName.fromElement(prop)] = prop.fromString(alarm)
+
+
     def isInbox(self):
         """
         Indicates whether this calendar is an "inbox".
@@ -1262,17 +1353,17 @@
 
     # Stuff from put_common
     @inlineCallbacks
-    def fullValidation(self, component, inserting, internal):
+    def fullValidation(self, component, inserting, update_state):
         """
         Do full validation of source and destination calendar data.
         """
 
         # Basic validation
         #TODO: figure out what to do about etag/schedule-tag
-        self.validIfScheduleMatch(False, False, internal)
+        self.validIfScheduleMatch(False, False, update_state)
 
-        # Skip validation on internal requests
-        if not internal:
+        # Do validation on external requests
+        if update_state == ComponentUpdateState.NORMAL:
 
             # Valid data sizes - do before parsing the data
             if config.MaxResourceSize:
@@ -1284,8 +1375,8 @@
         if config.EnableTimezonesByReference:
             component.stripKnownTimezones()
 
-        # Skip validation on internal requests
-        if not internal:
+        # Do validation on external requests
+        if update_state == ComponentUpdateState.NORMAL:
 
             # Valid calendar data checks
             yield self.validCalendarDataCheck(component, inserting)
@@ -1302,16 +1393,16 @@
             component.normalizeCalendarUserAddresses(normalizationLookup, self.calendar().viewerHome().principalForCalendarUserAddress)
 
         # Check access
-        yield self.validAccess(component, inserting, internal)
+        yield self.validAccess(component, inserting, update_state)
 
 
-    def validIfScheduleMatch(self, etag_match, schedule_tag, internal):
+    def validIfScheduleMatch(self, etag_match, schedule_tag, update_state):
         """
         Check for If-ScheduleTag-Match header behavior.
         """
         # Only when a direct request
         self.schedule_tag_match = False
-        if not self.calendar().isInbox() and not internal:
+        if not self.calendar().isInbox() and update_state == ComponentUpdateState.NORMAL:
             if schedule_tag:
                 self._validIfScheduleMatch(self.request)
                 self.schedule_tag_match = True
@@ -1380,7 +1471,7 @@
                     raise TooManyAttendeesError("Attendee list size %d is larger than allowed limit %d" % (attendeeListLength, config.MaxAttendeesPerInstance))
 
 
-    def validAccess(self, component, inserting, internal):
+    def validAccess(self, component, inserting, update_state):
         """
         Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with.
         """
@@ -1393,7 +1484,7 @@
                 raise InvalidCalendarAccessError("Private event access level not allowed")
 
             # Only DAV:owner is able to set the property to other than PUBLIC
-            if not internal:
+            if update_state == ComponentUpdateState.NORMAL:
                 if self.calendar().viewerHome().uid() != self._txn._authz_uid and access != Component.ACCESS_PUBLIC:
                     raise InvalidCalendarAccessError("Private event access level change not allowed")
 
@@ -1406,6 +1497,275 @@
 
 
     @inlineCallbacks
+    def preservePrivateComments(self, component, inserting):
+        """
+        Check for private comments on the old resource and the new resource and re-insert
+        ones that are lost.
+
+        NB Do this before implicit scheduling as we don't want old clients to trigger scheduling when
+        the X- property is missing.
+        """
+        if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
+            old_has_private_comments = not inserting and self.hasPrivateComment
+            new_has_private_comments = component.hasPropertyInAnyComponent((
+                "X-CALENDARSERVER-PRIVATE-COMMENT",
+                "X-CALENDARSERVER-ATTENDEE-COMMENT",
+            ))
+
+            if old_has_private_comments and not new_has_private_comments:
+                # Transfer old comments to new calendar
+                log.debug("Private Comments properties were entirely removed by the client. Restoring existing properties.")
+                old_calendar = (yield self.componentForUser())
+                component.transferProperties(old_calendar, (
+                    "X-CALENDARSERVER-PRIVATE-COMMENT",
+                    "X-CALENDARSERVER-ATTENDEE-COMMENT",
+                ))
+
+            self.hasPrivateComment = new_has_private_comments
+
+
+    @inlineCallbacks
+    def replaceMissingToDoProperties(self, calendar, inserting, update_state):
+        """
+        Recover any lost ORGANIZER or ATTENDEE properties in non-recurring VTODOs.
+        """
+
+        if not inserting and calendar.resourceType() == "VTODO" and not calendar.isRecurring():
+
+            old_calendar = (yield self.componentForUser())
+
+            new_organizer = calendar.getOrganizer()
+            old_organizer = old_calendar.getOrganizerProperty()
+            new_attendees = calendar.getAttendees()
+            old_attendees = tuple(old_calendar.getAllAttendeeProperties())
+
+            new_completed = calendar.mainComponent().hasProperty("COMPLETED")
+            old_completed = old_calendar.mainComponent().hasProperty("COMPLETED")
+
+            if old_organizer and not new_organizer and len(old_attendees) > 0 and len(new_attendees) == 0:
+                # Transfer old organizer and attendees to new calendar
+                log.debug("Organizer and attendee properties were entirely removed by the client. Restoring existing properties.")
+
+                # Get the originator who is the owner of the calendar resource being modified
+                originatorPrincipal = (yield self.calendar().ownerHome().principal())
+                originatorAddresses = originatorPrincipal.calendarUserAddresses()
+
+                for component in calendar.subcomponents():
+                    if component.name() != "VTODO":
+                        continue
+
+                    if not component.hasProperty("DTSTART"):
+                        # Need to put DTSTART back in or we get a date mismatch failure later
+                        for old_component in old_calendar.subcomponents():
+                            if old_component.name() != "VTODO":
+                                continue
+                            if old_component.hasProperty("DTSTART"):
+                                component.addProperty(old_component.getProperty("DTSTART").duplicate())
+                                break
+
+                    # Add organizer back in from previous resource
+                    component.addProperty(old_organizer.duplicate())
+
+                    # Add attendees back in from previous resource
+                    for anAttendee in old_attendees:
+                        anAttendee = anAttendee.duplicate()
+                        if component.hasProperty("COMPLETED") and anAttendee.value() in originatorAddresses:
+                            anAttendee.setParameter("PARTSTAT", "COMPLETED")
+                        component.addProperty(anAttendee)
+
+            elif new_completed ^ old_completed and update_state == ComponentUpdateState.NORMAL:
+                # COMPLETED changed - sync up attendee state
+                # We need this because many VTODO clients are not aware of scheduling,
+                # i.e. they do not adjust any ATTENDEE PARTSTATs. We are going to impose
+                # our own requirement that PARTSTAT is set to COMPLETED when the COMPLETED
+                # property is added.
+
+                # Transfer old organizer and attendees to new calendar
+                log.debug("Sync COMPLETED property change.")
+
+                # Get the originator who is the owner of the calendar resource being modified
+                originatorPrincipal = (yield self.calendar().ownerHome().principal())
+                originatorAddresses = originatorPrincipal.calendarUserAddresses()
+
+                for component in calendar.subcomponents():
+                    if component.name() != "VTODO":
+                        continue
+
+                    # Change owner partstat
+                    for anAttendee in component.properties("ATTENDEE"):
+                        if anAttendee.value() in originatorAddresses:
+                            oldpartstat = anAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
+                            newpartstat = "COMPLETED" if component.hasProperty("COMPLETED") else "IN-PROCESS"
+                            if newpartstat != oldpartstat:
+                                anAttendee.setParameter("PARTSTAT", newpartstat)
+
+
+    @inlineCallbacks
+    def dropboxPathNormalization(self, component):
+        """
+        Make sure sharees only use dropbox paths of the sharer.
+        """
+
+        # Only relevant if calendar is sharee collection
+        if not self.calendar().owned():
+
+            # Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
+            xdropboxes = component.getAllPropertiesInAnyComponent(
+                "X-APPLE-DROPBOX",
+                depth=1,
+            )
+            attachments = component.getAllPropertiesInAnyComponent(
+                "ATTACH",
+                depth=1,
+            )
+            attachments = [
+                attachment for attachment in attachments
+                if attachment.parameterValue("VALUE", "URI") == "URI" and attachment.value().startswith("http")
+            ]
+
+            if len(xdropboxes) or len(attachments):
+
+                # Determine owner GUID
+                owner = (yield self.calendar().ownerHome()).uid()
+
+                def uriNormalize(uri):
+                    urichanged = False
+                    scheme, netloc, path, params, query, fragment = urlparse(uri)
+                    pathbits = path.split("/")
+                    if len(pathbits) >= 6 and pathbits[4] == "dropbox":
+                        if pathbits[1] != "calendars":
+                            pathbits[1] = "calendars"
+                            urichanged = True
+                        if pathbits[2] != "__uids__":
+                            pathbits[2] = "__uids__"
+                            urichanged = True
+                        if pathbits[3] != owner:
+                            pathbits[3] = owner
+                            urichanged = True
+                        if urichanged:
+                            return urlunparse((scheme, netloc, "/".join(pathbits), params, query, fragment,))
+                    return None
+
+                for xdropbox in xdropboxes:
+                    uri = uriNormalize(xdropbox.value())
+                    if uri:
+                        xdropbox.setValue(uri)
+                        self._componentChanged = True
+                for attachment in attachments:
+                    uri = uriNormalize(attachment.value())
+                    if uri:
+                        attachment.setValue(uri)
+                        self._componentChanged = True
+
+
+    def processAlarms(self, component, inserting):
+        """
+        Remove duplicate alarms. Add a default alarm if required.
+
+        @return: indicate whether a change was made
+        @rtype: C{bool}
+        """
+
+        # Remove duplicate alarms
+        if config.RemoveDuplicateAlarms and component.hasDuplicateAlarms(doFix=True):
+            self._componentChanged = True
+
+        # Only if feature enabled
+        if not config.EnableDefaultAlarms:
+            return
+
+        # Check that we are creating and this is not the inbox
+        if not inserting or self.calendar().isInbox():
+            return
+
+        # Never add default alarms to calendar data in shared calendars
+        if not self.calendar().owned():
+            return
+
+        # Add default alarm for VEVENT and VTODO only
+        mtype = component.mainType().upper()
+        if component.mainType().upper() not in ("VEVENT", "VTODO"):
+            return
+        vevent = mtype == "VEVENT"
+
+        # Check timed or all-day
+        start, _ignore_end = component.mainComponent(allow_multiple=True).getEffectiveStartEnd()
+        if start is None:
+            # Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
+            return
+        timed = not start.isDateOnly()
+
+        # See if default exists and add using appropriate logic
+        alarm = self.calendar().getDefaultAlarm(vevent, timed)
+        if alarm and component.addAlarms(alarm):
+            self._componentChanged = True
+
+
+    @inlineCallbacks
+    def mergePerUserData(self, component, inserting):
+        """
+        Merge incoming calendar data with other user's per-user data in existing calendar data.
+
+        @param component: incoming calendar data
+        @type component: L{twistedcaldav.ical.Component}
+        """
+        accessUID = self.calendar().viewerHome().uid()
+        oldCal = (yield self.component()) if not inserting else None
+
+        # Duplicate before we do the merge because someone else may "own" the calendar object
+        # and we should not change it. This is not ideal as we may duplicate it unnecessarily
+        # but we currently have no api to let the caller tell us whether it cares about the
+        # whether the calendar data is changed or not.
+        try:
+            component = PerUserDataFilter(accessUID).merge(component.duplicate(), oldCal)
+        except ValueError:
+            log.err("Invalid per-user data merge")
+            raise InvalidPerUserDataMerge("Cannot merge per-user data")
+
+        returnValue(component)
+
+
+    def processScheduleTags(self, component, inserting, update_state):
+
+        # Check for scheduling object resource and write property
+        if self.isScheduleObject:
+            # Need to figure out when to change the schedule tag:
+            #
+            # 1. If this is not an internal request then the resource is being explicitly changed
+            # 2. If it is an internal request for the Organizer, schedule tag never changes
+            # 3. If it is an internal request for an Attendee and the message being processed came
+            #    from the Organizer then the schedule tag changes.
+
+            # Check what kind of processing is going on
+            change_scheduletag = update_state not in (
+                ComponentUpdateState.ORGANIZER_ITIP_UPDATE,
+                ComponentUpdateState.ATTENDEE_ITIP_REFRESH,
+            )
+
+            if change_scheduletag or not self.scheduleTag:
+                self.scheduleTag = str(uuid.uuid4())
+
+            # Handle weak etag compatibility
+            if config.Scheduling.CalDAV.ScheduleTagCompatibility:
+                if change_scheduletag:
+                    # Schedule-Tag change => weak ETag behavior must not happen
+                    etags = ()
+                else:
+                    # Schedule-Tag did not change => add current ETag to list of those that can
+                    # be used in a weak precondition test
+                    etags = self.scheduleEtags
+                    if etags is None:
+                        etags = ()
+                etags += (self._generateEtag(str(component)),)
+                self.scheduleEtags = etags
+            else:
+                self.scheduleEtags = ()
+        else:
+            self.scheduleTag = ""
+            self.scheduleEtags = ()
+
+
+    @inlineCallbacks
     def _lockUID(self, component, inserting):
         """
         Create a lock on the component's UID and verify, after getting the lock, that the incoming UID
@@ -1436,25 +1796,77 @@
         Scheduling will be done automatically.
         """
 
-        return self._setComponentInternal(component, inserting, False)
+        return self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL)
 
 
     @inlineCallbacks
-    def _setComponentInternal(self, component, inserting=False, internal=True):
+    def _setComponentInternal(self, component, inserting=False, update_state=ComponentUpdateState.NORMAL):
         """
         Setting the component internally to the store itself. This will bypass a whole bunch of data consistency checks
         on the assumption that those have been done prior to the component data being provided, provided the flag is set.
         This should always be treated as an api private to the store.
         """
 
+        self._componentChanged = False
+
         # Handle all validation operations here.
-        yield self.fullValidation(component, inserting, internal)
+        yield self.fullValidation(component, inserting, update_state)
 
         # UID lock - this will remain active until the end of the current txn
         yield self._lockUID(component, inserting)
 
+        # Preserve private comments
+        yield self.preservePrivateComments(component, inserting)
+
+        # Fix broken VTODOs
+        yield self.replaceMissingToDoProperties(component, inserting, update_state)
+
+        # Handle sharing dropbox normalization
+        yield self.dropboxPathNormalization(component)
+
+        # Pre-process managed attachments
+        if update_state == ComponentUpdateState.NORMAL:
+            managed_copied, managed_removed = (yield self.updatingResourceCheckAttachments(component, inserting))
+
+        # Default/duplicate alarms
+        self.processAlarms(component, inserting)
+
+#        # Do scheduling
+#        implicit_result = (yield self.doImplicitScheduling())
+#        if isinstance(implicit_result, int):
+#            if implicit_result == ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT:
+#                raise ResourceDeletedError("Resource created but immediately deleted by the server.")
+#
+#            elif implicit_result == ImplicitScheduler.STATUS_ORPHANED_EVENT:
+#
+#                # Now forcibly delete the event
+#                if not inserting:
+#                    yield self.storeRemove()
+#                    raise ResourceDeletedError("Resource modified but immediately deleted by the server.")
+#                else:
+#                    raise AttendeeAllowedError("Attendee cannot create event for Organizer: %s" % (implicit_result,))
+#
+#            else:
+#                msg = "Invalid return status code from ImplicitScheduler: %s" % (implicit_result,)
+#                log.err(msg)
+#                raise InvalidObjectResourceError(msg)
+#        else:
+#            self.isScheduleObject, data_changed, did_implicit_action = implicit_result
+
+        # Always do the per-user data merge right before we store
+        component = (yield self.mergePerUserData(component, inserting))
+
+        self.processScheduleTags(component, inserting, update_state)
+
         yield self.updateDatabase(component, inserting=inserting)
 
+        # Post process managed attachments
+        if update_state == ComponentUpdateState.NORMAL:
+            if managed_copied:
+                yield self.copyResourceAttachments(managed_copied)
+            if managed_removed:
+                yield self.removeResourceAttachments(managed_removed)
+
         if inserting:
             yield self._calendar._insertRevision(self._name)
         else:
@@ -1462,7 +1874,13 @@
 
         yield self._calendar.notifyChanged()
 
+        returnValue(self._componentChanged)
 
+
+    def _generateEtag(self, componentText):
+        return hashlib.md5(componentText + (self.scheduleTag if self.scheduleTag else "")).hexdigest()
+
+
     @inlineCallbacks
     def updateDatabase(self, component, expand_until=None, reCreate=False,
                        inserting=False, txn=None):
@@ -1579,7 +1997,7 @@
 
             # CALENDAR_OBJECT table update
             self._uid = component.resourceUID()
-            self._md5 = hashlib.md5(componentText + (self._schedule_tag if self._schedule_tag else "")).hexdigest()
+            self._md5 = self._generateEtag(componentText)
             self._size = len(componentText)
 
             # Special - if migrating we need to preserve the original md5
@@ -1803,11 +2221,11 @@
         """
 
         if user_uuid is None:
-            user_uuid = self._parentCollection.viewHome().uid()
+            user_uuid = self._parentCollection.viewerHome().uid()
 
         if user_uuid not in self._cachedCommponentPerUser:
             caldata = yield self.component()
-            filtered = PerUserDataFilter(user_uuid).filter(caldata)
+            filtered = PerUserDataFilter(user_uuid).filter(caldata.duplicate())
             self._cachedCommponentPerUser[user_uuid] = filtered
         returnValue(self._cachedCommponentPerUser[user_uuid])
 
@@ -1994,7 +2412,7 @@
 
 
     @inlineCallbacks
-    def updatingResourceCheckAttachments(self, component):
+    def updatingResourceCheckAttachments(self, component, inserting=False):
         """
         A component is being changed. Check any ATTACH properties that may be present
         to verify they are owned by the organizer/owner of the resource and re-write the managed-ids.
@@ -2012,13 +2430,16 @@
                 newattached[managed_id].append(attachment)
 
         # Retrieve all ATTACH properties with a MANAGED-ID in old data
-        oldcomponent = (yield self.component())
-        oldattached = collections.defaultdict(list)
-        oldattachments = oldcomponent.getAllPropertiesInAnyComponent("ATTACH", depth=1,)
-        for attachment in oldattachments:
-            managed_id = attachment.parameterValue("MANAGED-ID")
-            if managed_id is not None:
-                oldattached[managed_id].append(attachment)
+        if not inserting:
+            oldcomponent = (yield self.component())
+            oldattached = collections.defaultdict(list)
+            oldattachments = oldcomponent.getAllPropertiesInAnyComponent("ATTACH", depth=1,)
+            for attachment in oldattachments:
+                managed_id = attachment.parameterValue("MANAGED-ID")
+                if managed_id is not None:
+                    oldattached[managed_id].append(attachment)
+        else:
+            oldattached = collections.defaultdict(list)
 
         # Punt if no managed attachments
         if len(newattached) + len(oldattached) == 0:

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py	2013-04-10 21:28:48 UTC (rev 11025)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/datastore/test/test_implicit.py	2013-04-10 21:43:39 UTC (rev 11026)
@@ -28,8 +28,9 @@
     InvalidObjectResourceError
 from txdav.caldav.icalendarstore import InvalidComponentTypeError, \
     TooManyAttendeesError, InvalidCalendarAccessError, InvalidUIDError, \
-    UIDExistsError
+    UIDExistsError, ComponentUpdateState
 import sys
+from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
 
 class ImplicitRequests (CommonCommonTests, TestCase):
     """
@@ -102,9 +103,10 @@
 
         calendar_resource1 = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
         calendar1 = (yield calendar_resource1.component())
-        self.assertTrue("urn:uuid:user01" in str(calendar1))
-        self.assertTrue("urn:uuid:user02" in str(calendar1))
-        self.assertTrue("CN=" in str(calendar1))
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("urn:uuid:user01" in calendar1)
+        self.assertTrue("urn:uuid:user02" in calendar1)
+        self.assertTrue("CN=" in calendar1)
         yield self.commit()
 
 
@@ -388,8 +390,9 @@
 
         calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
         calendar1 = (yield calendar_resource.component())
-        self.assertTrue("X-CALENDARSERVER-ACCESS:PRIVATE" in str(calendar1))
-        self.assertTrue("SUMMARY:Changed" in str(calendar1))
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("X-CALENDARSERVER-ACCESS:PRIVATE" in calendar1)
+        self.assertTrue("SUMMARY:Changed" in calendar1)
         yield self.commit()
 
 
@@ -541,3 +544,576 @@
         else:
             self.fail("Exception not raised")
         yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_preservePrivateComments(self):
+        """
+        Test that resource private comments are restored.
+        """
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+X-CALENDARSERVER-PRIVATE-COMMENT:My Comment
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        data2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Changed
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar = Component.fromString(data2)
+        txn = self.transactionUnderTest()
+        txn._authz_uid = "user01"
+        yield calendar_resource.setComponent(calendar)
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("X-CALENDARSERVER-PRIVATE-COMMENT:My Comment" in calendar1)
+        self.assertTrue("SUMMARY:Changed" in calendar1)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_replaceMissingToDoProperties_OrganizerAttendee(self):
+        """
+        Test that missing scheduling properties in VTODOs are recovered.
+        """
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+END:VTODO
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        data2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Changed
+END:VTODO
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar = Component.fromString(data2)
+        txn = self.transactionUnderTest()
+        txn._authz_uid = "user01"
+        yield calendar_resource.setComponent(calendar)
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("ORGANIZER" in calendar1)
+        self.assertTrue("ATTENDEE" in calendar1)
+        self.assertTrue("SUMMARY:Changed" in calendar1)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_replaceMissingToDoProperties_Completed(self):
+        """
+        Test that VTODO completed status is fixed.
+        """
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+END:VTODO
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        data2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Changed
+COMPLETED:20080601T140000Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+END:VTODO
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar = Component.fromString(data2)
+        txn = self.transactionUnderTest()
+        txn._authz_uid = "user01"
+        yield calendar_resource.setComponent(calendar)
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("ORGANIZER" in calendar1)
+        self.assertTrue("ATTENDEE" in calendar1)
+        self.assertTrue("SUMMARY:Changed" in calendar1)
+        self.assertTrue("PARTSTAT=COMPLETED" in calendar1)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_dropboxPathNormalization(self):
+        """
+        Test that dropbox paths are normalized.
+        """
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        sharee_home = (yield self.homeUnderTest(name="user02"))
+        shared_name = (yield calendar_collection.shareWith(sharee_home, _BIND_MODE_WRITE,))
+        yield self.commit()
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTODO
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+X-APPLE-DROPBOX:https://example.com/calendars/users/user02/dropbox/123.dropbox
+ATTACH;VALUE=URI:https://example.com/calendars/users/user02/dropbox/123.dropbox/1.txt
+ATTACH;VALUE=URI:https://example.org/attachments/2.txt
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+END:VTODO
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(name=shared_name, home="user02"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", calendar_name=shared_name, home="user02",))
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("X-APPLE-DROPBOX:https://example.com/calendars/__uids__/user01/dropbox/123.dropbox" in calendar1)
+        self.assertTrue("ATTACH:https://example.com/calendars/__uids__/user01/dropbox/123.dropbox/1.txt" in calendar1)
+        self.assertTrue("ATTACH:https://example.org/attachments/2.txt" in calendar1)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_processAlarms_DuplicateRemoval(self):
+        """
+        Test that duplicate alarms are removed.
+        """
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        data2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Changed
+BEGIN:VALARM
+X-WR-ALARMUID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+BEGIN:VALARM
+X-WR-ALARMUID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar = Component.fromString(data2)
+        txn = self.transactionUnderTest()
+        txn._authz_uid = "user01"
+        result = (yield calendar_resource.setComponent(calendar))
+        yield self.commit()
+        self.assertTrue(result)
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 1)
+        self.assertTrue("SUMMARY:Changed" in calendar1)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_processAlarms_AddDefault(self):
+        """
+        Test that default alarms are added.
+        """
+
+        alarm = """BEGIN:VALARM
+X-WR-ALARMUID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+"""
+
+        home = (yield self.homeUnderTest(name="user01"))
+        home.setDefaultAlarm(alarm, True, True)
+        yield self.commit()
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 1)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_processAlarms_NoDefaultShared(self):
+        """
+        Test that default alarms are not added to shared resources.
+        """
+
+        # Set default alarm for user02
+        alarm = """BEGIN:VALARM
+X-WR-ALARMUID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+"""
+
+        home = (yield self.homeUnderTest(name="user02"))
+        home.setDefaultAlarm(alarm, True, True)
+        yield self.commit()
+
+        # user01 shares calendar with user02
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        sharee_home = (yield self.homeUnderTest(name="user02"))
+        shared_name = (yield calendar_collection.shareWith(sharee_home, _BIND_MODE_WRITE,))
+        yield self.commit()
+
+        # user02 writes event to shared calendar
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(name=shared_name, home="user02"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", calendar_name=shared_name, home="user02",))
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 0)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_mergePerUserData(self):
+        """
+        Test that per-user data is correctly stored and retrieved.
+        """
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        sharee_home = (yield self.homeUnderTest(name="user02"))
+        shared_name = (yield calendar_collection.shareWith(sharee_home, _BIND_MODE_WRITE,))
+        yield self.commit()
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+BEGIN:VALARM
+X-WR-ALARMUID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT5M
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        data2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+BEGIN:VALARM
+X-WR-ALARMUID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT10M
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", calendar_name=shared_name, home="user02",))
+        calendar = Component.fromString(data2)
+        yield calendar_resource.setComponent(calendar)
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+
+        # Unfiltered view of event
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("TRIGGER:-PT5M" in calendar1)
+        self.assertTrue("TRIGGER:-PT10M" in calendar1)
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 2)
+
+        # user01 view of event
+        calendar1 = (yield calendar_resource.componentForUser("user01"))
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("TRIGGER:-PT5M" in calendar1)
+        self.assertFalse("TRIGGER:-PT10M" in calendar1)
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 1)
+
+        # user02 view of event
+        calendar1 = (yield calendar_resource.componentForUser("user02"))
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertFalse("TRIGGER:-PT5M" in calendar1)
+        self.assertTrue("TRIGGER:-PT10M" in calendar1)
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 1)
+
+        yield self.commit()
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", calendar_name=shared_name, home="user02",))
+
+        # Unfiltered view of event
+        calendar1 = (yield calendar_resource.component())
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("TRIGGER:-PT5M" in calendar1)
+        self.assertTrue("TRIGGER:-PT10M" in calendar1)
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 2)
+
+        # user01 view of event
+        calendar1 = (yield calendar_resource.componentForUser("user01"))
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertTrue("TRIGGER:-PT5M" in calendar1)
+        self.assertFalse("TRIGGER:-PT10M" in calendar1)
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 1)
+
+        # user02 view of event
+        calendar1 = (yield calendar_resource.componentForUser("user02"))
+        calendar1 = str(calendar1).replace("\r\n ", "")
+        self.assertFalse("TRIGGER:-PT5M" in calendar1)
+        self.assertTrue("TRIGGER:-PT10M" in calendar1)
+        self.assertEqual(calendar1.count("BEGIN:VALARM"), 1)
+
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_validation_processScheduleTags(self):
+        """
+        Test that schedule tags are correctly updated.
+        """
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+X-CALENDARSERVER-PRIVATE-COMMENT:My Comment
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_collection = (yield self.calendarUnderTest(home="user01"))
+        calendar = Component.fromString(data1)
+        yield calendar_collection.createCalendarObjectWithName("test.ics", calendar)
+        yield self.commit()
+
+        data2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+SUMMARY:Changed #1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar_resource.isScheduleObject = True
+        calendar = Component.fromString(data2)
+        yield calendar_resource.setComponent(calendar)
+        schedule_tag = calendar_resource.scheduleTag
+        yield self.commit()
+
+        data3 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+SUMMARY:Changed #2
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar = Component.fromString(data3)
+        yield calendar_resource.setComponent(calendar)
+        self.assertNotEqual(calendar_resource.scheduleTag, schedule_tag)
+        schedule_tag = calendar_resource.scheduleTag
+        yield self.commit()
+
+        data4 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-attendee-reply
+DTSTAMP:20080601T120000Z
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+SUMMARY:Changed #2
+END:VEVENT
+END:VCALENDAR
+"""
+
+        calendar_resource = (yield self.calendarObjectUnderTest(name="test.ics", home="user01",))
+        calendar = Component.fromString(data4)
+        yield calendar_resource._setComponentInternal(calendar, update_state=ComponentUpdateState.ORGANIZER_ITIP_UPDATE)
+        self.assertEqual(calendar_resource.scheduleTag, schedule_tag)
+        yield self.commit()

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-10 21:28:48 UTC (rev 11025)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/caldav/icalendarstore.py	2013-04-10 21:43:39 UTC (rev 11026)
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from twisted.python.constants import NamedConstant, Names
 
 """
 Calendar store interfaces
@@ -55,7 +56,40 @@
 ]
 
 
+class ComponentUpdateState(Names):
+    """
+    These are constants that define what type of component store operation is being done. This is used
+    in the .setComponent() api to determine what type of processing needs to occur.
 
+    NORMAL -                this is an application layer (user) generated store that should do all
+                            validation and implicit scheduling operations.
+
+    ORGANIZER_ITIP_UPDATE - the store is an update to an organizer's data caused by processing an incoming
+                            iTIP message. Some validation and implicit scheduling is not done. Schedule-Tag
+                            is not changed.
+
+    ATTENDEE_ITIP_UPDATE  - the store is an update to an attendee's data caused by processing an incoming
+                            iTIP message. Some validation and implicit scheduling is not done. Schedule-Tag
+                            is changed.
+
+    ATTENDEE_ITIP_REFRESH - the store is an update to an attendee's data caused by processing an incoming
+                            iTIP message. Some validation and implicit scheduling is not done. Schedule-Tag
+                            is changed.
+
+    """
+
+    NORMAL = NamedConstant()
+    ORGANIZER_ITIP_UPDATE = NamedConstant()
+    ATTENDEE_ITIP_UPDATE = NamedConstant()
+    ATTENDEE_ITIP_REFRESH = NamedConstant()
+
+    NORMAL.description = "normal"
+    ORGANIZER_ITIP_UPDATE.description = "organizer-update"
+    ATTENDEE_ITIP_UPDATE.description = "attendee-update"
+    ATTENDEE_ITIP_REFRESH.description = "attendee-refresh"
+
+
+
 class InvalidComponentTypeError(CommonStoreError):
     """
     Invalid object resource component type for collection.
@@ -91,6 +125,27 @@
 
 
 
+class ResourceDeletedError(CommonStoreError):
+    """
+    The resource was determined to be redundant and was deleted by the server.
+    """
+
+
+
+class AttendeeAllowedError(CommonStoreError):
+    """
+    Attendee is not allowed to make an implicit scheduling change.
+    """
+
+
+
+class InvalidPerUserDataMerge(CommonStoreError):
+    """
+    Per-user data merge failed.
+    """
+
+
+
 class AttachmentStoreFailed(Exception):
     """
     Unable to store an attachment.

Modified: CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py	2013-04-10 21:28:48 UTC (rev 11025)
+++ CalendarServer/branches/users/cdaboo/store-scheduling/txdav/common/datastore/sql.py	2013-04-10 21:43:39 UTC (rev 11026)
@@ -3366,6 +3366,14 @@
         return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
 
 
+    @inlineCallbacks
+    def invalidateQueryCache(self):
+        queryCacher = self._txn._queryCacher
+        if queryCacher is not None:
+            cacheKey = queryCacher.keyForHomeChildMetaData(self._resourceID)
+            yield queryCacher.invalidateAfterCommit(self._txn, cacheKey)
+
+
     def exists(self):
         """
         An empty resource-id means this object does not yet exist in the DB.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130410/908d9c9c/attachment-0001.html>


More information about the calendarserver-changes mailing list