[CalendarServer-changes] [14166] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Nov 13 12:05:54 PST 2014


Revision: 14166
          http://trac.calendarserver.org//changeset/14166
Author:   sagen at apple.com
Date:     2014-11-13 12:05:54 -0800 (Thu, 13 Nov 2014)
Log Message:
-----------
Use iTip reply processing to restore events for which the imported is an ATTENDEE

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tools/importer.py
    CalendarServer/trunk/calendarserver/tools/test/test_importer.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py

Modified: CalendarServer/trunk/calendarserver/tools/importer.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/importer.py	2014-11-13 19:57:52 UTC (rev 14165)
+++ CalendarServer/trunk/calendarserver/tools/importer.py	2014-11-13 20:05:54 UTC (rev 14166)
@@ -40,10 +40,13 @@
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 from twistedcaldav.timezones import TimezoneCache
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
+from txdav.caldav.datastore.scheduling.itip import iTipGenerator
+from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor
+from txdav.caldav.icalendarstore import ComponentUpdateState
 from txdav.common.icommondatastore import UIDExistsError
 from txdav.xml import element as davxml
 
-
 log = Logger()
 
 
@@ -187,6 +190,7 @@
     home = yield txn.calendarHomeWithUID(ownerUID, create=True)
     collection = yield home.childWithName(collectionResourceName)
     if not collection:
+        print("Creating calendar: {}".format(collectionResourceName))
         collection = yield home.createChildWithName(collectionResourceName)
     for propertyName, element in (
         ("NAME", davxml.DisplayName),
@@ -195,6 +199,9 @@
         value = component.propertyValue(propertyName)
         if value is not None:
             setCollectionPropertyValue(collection, element, value)
+            print(
+                "Setting {name} to {value}".format(name=propertyName, value=value)
+            )
     yield txn.commit()
 
     # Populate the collection; NB we use a txn for each object, and we might
@@ -205,11 +212,12 @@
         try:
             uid = list(groupedComponent.subcomponents())[0].propertyValue("UID")
         except:
-            uid = "unknown"
+            continue
 
         # If event is unscheduled or the organizer matches homeUID, store the
         # component
 
+        print("Event UID: {}".format(uid))
         storeDirectly = True
         organizer = groupedComponent.getOrganizer()
         if organizer is not None:
@@ -226,7 +234,8 @@
             resourceName = "{}.ics".format(str(uuid.uuid4()))
             try:
                 yield storeComponentInHomeAndCalendar(
-                    store, groupedComponent, ownerUID, collectionResourceName, resourceName
+                    store, groupedComponent, ownerUID, collectionResourceName,
+                    resourceName
                 )
                 print("Imported: {}".format(uid))
             except UIDExistsError:
@@ -242,15 +251,45 @@
                 )
 
         else:
-            # Owner is not the organizer
-            ownerCUA = ownerRecord.canonicalCalendarUserAddress()
-            yield reInviteAttendee(store, organizerRecord.uid, uid, ownerCUA)
+            # Owner is an attendee, not the organizer
+            # Apply the PARTSTATs from the import and from the possibly
+            # existing event (existing event takes precedence) to the
+            # organizer's copy.
 
+            # Put the attendee copy into the right calendar now otherwise it
+            # could end up on the default calendar when the change to the
+            # organizer's copy causes an attendee update
+            resourceName = "{}.ics".format(str(uuid.uuid4()))
+            try:
+                yield storeComponentInHomeAndCalendar(
+                    store, groupedComponent, ownerUID, collectionResourceName,
+                    resourceName, asAttendee=True
+                )
+                print("Imported: {}".format(uid))
+            except UIDExistsError:
+                # No need since the event is already in the home
+                pass
 
+            # Now use the iTip reply processing to update the organizer's copy
+            # with the PARTSTATs from the component we're restoring.
+            attendeeCUA = ownerRecord.canonicalCalendarUserAddress()
+            organizerCUA = organizerRecord.canonicalCalendarUserAddress()
+            processor = ImplicitProcessor()
+            newComponent = iTipGenerator.generateAttendeeReply(groupedComponent, attendeeCUA, method="X-RESTORE")
+            txn = store.newTransaction()
+            yield processor.doImplicitProcessing(
+                txn,
+                newComponent,
+                LocalCalendarUser(attendeeCUA, ownerRecord),
+                LocalCalendarUser(organizerCUA, organizerRecord)
+            )
+            yield txn.commit()
 
+
 @inlineCallbacks
 def storeComponentInHomeAndCalendar(
-    store, component, homeUID, collectionResourceName, objectResourceName
+    store, component, homeUID, collectionResourceName, objectResourceName,
+    asAttendee=False
 ):
     """
     Add a component to the store as an objectResource
@@ -276,50 +315,18 @@
     if not collection:
         collection = yield home.createChildWithName(collectionResourceName)
 
-    yield collection.createObjectResourceWithName(objectResourceName, component)
+    yield collection._createCalendarObjectWithNameInternal(
+        objectResourceName, component,
+        (
+            ComponentUpdateState.ATTENDEE_ITIP_UPDATE
+            if asAttendee else
+            ComponentUpdateState.NORMAL
+        )
+    )
     yield txn.commit()
 
 
- at inlineCallbacks
-def reInviteAttendee(
-    store, organizerUID, objectUID, attendeeCUA
-):
-    """
-    Force a re-invite of an attendee
 
-    Find the event with the given objectUID within the calendar home for
-    organizerUID, and update it so the attendeeCUA's PARTSTAT is set to
-    NEEDS-ACTION.
-
-    @param store: The db store to add the component to
-    @type store: L{IDataStore}
-    @param organizerUID: uid of the organizer
-    @type organizerUID: C{str}
-    @param objectUID: uid of the event
-    @type objectUID: C{str}
-    @param attendeeCUA: CUA of the attendee to re-invite
-    @type attendeeCUA: C{str}
-    """
-
-    txn = store.newTransaction()
-    organizerHome = yield txn.calendarHomeWithUID(organizerUID)
-    if organizerHome is not None:
-        for object in (yield organizerHome.getCalendarResourcesForUID(objectUID)):
-            component = yield object.componentForUser()
-
-            # duplicate() so implicit actions will trigger -- otherwise
-            # the data objects will be the same when you call setComponent
-            component = component.duplicate()
-
-            for attendeeProp in (yield component.getAttendeeProperties((attendeeCUA,))):
-                if attendeeProp is not None:
-                    attendeeProp.setParameter("PARTSTAT", "NEEDS-ACTION")
-                    yield object.setComponent(component)
-                    print("Reinviting to: {}".format(objectUID))
-
-    yield txn.commit()
-
-
 class ImporterService(WorkerService, object):
     """
     Service which runs, imports the data, then stops the reactor.

Modified: CalendarServer/trunk/calendarserver/tools/test/test_importer.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_importer.py	2014-11-13 19:57:52 UTC (rev 14165)
+++ CalendarServer/trunk/calendarserver/tools/test/test_importer.py	2014-11-13 20:05:54 UTC (rev 14166)
@@ -153,8 +153,9 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:6DB84FB1-C943-4144-BE65-9B0DD9A9E2C7
-DTSTART;TZID=America/Los_Angeles:20151008T053000
-DTEND;TZID=America/Los_Angeles:20151008T070000
+DTSTART;TZID=America/Los_Angeles:20141115T074500
+DTEND;TZID=America/Los_Angeles:20141115T091500
+RRULE:FREQ=DAILY;COUNT=20
 ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL:urn:x-uid:user01
 ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;ROLE=CHAIR:urn:x-uid:user02
 ATTENDEE;CN=User 03;CUTYPE=INDIVIDUAL:urn:x-uid:user03
@@ -167,9 +168,71 @@
 SUMMARY:Other organizer
 TRANSP:OPAQUE
 END:VEVENT
+BEGIN:VEVENT
+UID:6DB84FB1-C943-4144-BE65-9B0DD9A9E2C7
+RECURRENCE-ID;TZID=America/Los_Angeles:20141117T074500
+DTSTART;TZID=America/Los_Angeles:20141117T093000
+DTEND;TZID=America/Los_Angeles:20141117T110000
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL:urn:x-uid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;ROLE=CHAIR:urn:x-uid:user02
+ATTENDEE;CN=User 03;CUTYPE=INDIVIDUAL:urn:x-uid:user03
+ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM:urn:x-uid:mercury
+CREATED:20141107T172645Z
+DTSTAMP:20141107T172645Z
+LOCATION:Mercury
+ORGANIZER;CN=User 02:urn:x-uid:user02
+SEQUENCE:0
+SUMMARY:Other organizer
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+UID:6DB84FB1-C943-4144-BE65-9B0DD9A9E2C7
+RECURRENCE-ID;TZID=America/Los_Angeles:20141119T074500
+DTSTART;TZID=America/Los_Angeles:20141119T103000
+DTEND;TZID=America/Los_Angeles:20141119T120000
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL:urn:x-uid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;ROLE=CHAIR:urn:x-uid:user02
+ATTENDEE;CN=User 03;CUTYPE=INDIVIDUAL:urn:x-uid:user03
+ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM:urn:x-uid:mercury
+CREATED:20141107T172645Z
+DTSTAMP:20141107T172645Z
+LOCATION:Mercury
+ORGANIZER;CN=User 02:urn:x-uid:user02
+SEQUENCE:0
+SUMMARY:Other organizer
+TRANSP:OPAQUE
+END:VEVENT
 END:VCALENDAR
 """
 
+# DATA_USER02_INVITES_USER01_ATTENDEE_COPY = """BEGIN:VCALENDAR
+# VERSION:2.0
+# NAME:I'm an attendee
+# COLOR:#0000FFFF
+# SOURCE;VALUE=URI:http://example.com/calendars/__uids__/user01/calendar/
+# PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+# BEGIN:VEVENT
+# UID:6DB84FB1-C943-4144-BE65-9B0DD9A9E2C7
+# DTSTART;TZID=America/Los_Angeles:20151008T053000
+# DTEND;TZID=America/Los_Angeles:20151008T070000
+# ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:x-uid:user01
+# ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;ROLE=CHAIR:urn:x-uid:user02
+# ATTENDEE;CN=User 03;CUTYPE=INDIVIDUAL:urn:x-uid:user03
+# ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM:urn:x-uid:mercury
+# CREATED:20141107T172645Z
+# DTSTAMP:20141107T172645Z
+# LOCATION:Mercury
+# ORGANIZER;CN=User 02:urn:x-uid:user02
+# SEQUENCE:0
+# SUMMARY:Other organizer
+# TRANSP:OPAQUE
+# END:VEVENT
+# END:VCALENDAR
+# """
+
+# FIXME: add overrides on both attendee and organizer that aren't in the other
+
+
 DATA_USER02_INVITES_USER01_ATTENDEE_COPY = """BEGIN:VCALENDAR
 VERSION:2.0
 NAME:I'm an attendee
@@ -178,9 +241,10 @@
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VEVENT
 UID:6DB84FB1-C943-4144-BE65-9B0DD9A9E2C7
-DTSTART;TZID=America/Los_Angeles:20151008T053000
-DTEND;TZID=America/Los_Angeles:20151008T070000
-ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL:urn:x-uid:user01
+DTSTART;TZID=America/Los_Angeles:20141115T074500
+DTEND;TZID=America/Los_Angeles:20141115T091500
+RRULE:FREQ=DAILY;COUNT=20
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:x-uid:user01
 ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;ROLE=CHAIR:urn:x-uid:user02
 ATTENDEE;CN=User 03;CUTYPE=INDIVIDUAL:urn:x-uid:user03
 ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM:urn:x-uid:mercury
@@ -192,10 +256,87 @@
 SUMMARY:Other organizer
 TRANSP:OPAQUE
 END:VEVENT
+BEGIN:VEVENT
+UID:6DB84FB1-C943-4144-BE65-9B0DD9A9E2C7
+RECURRENCE-ID;TZID=America/Los_Angeles:20141117T074500
+DTSTART;TZID=America/Los_Angeles:20141117T093000
+DTEND;TZID=America/Los_Angeles:20141117T110000
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;PARTSTAT=TENTATIVE:urn:x-uid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;ROLE=CHAIR:urn:x-uid:user02
+ATTENDEE;CN=User 03;CUTYPE=INDIVIDUAL:urn:x-uid:user03
+ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM:urn:x-uid:mercury
+CREATED:20141107T172645Z
+DTSTAMP:20141107T172645Z
+LOCATION:Mercury
+ORGANIZER;CN=User 02:urn:x-uid:user02
+SEQUENCE:0
+SUMMARY:Other organizer
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+UID:6DB84FB1-C943-4144-BE65-9B0DD9A9E2C7
+RECURRENCE-ID;TZID=America/Los_Angeles:20141121T074500
+DTSTART;TZID=America/Los_Angeles:20141121T101500
+DTEND;TZID=America/Los_Angeles:20141121T114500
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;ROLE=CHAIR:urn:x-uid:user02
+ATTENDEE;CN=User 03;CUTYPE=INDIVIDUAL:urn:x-uid:user03
+ATTENDEE;CN=Mercury Seven;CUTYPE=ROOM:urn:x-uid:mercury
+CREATED:20141107T172645Z
+DTSTAMP:20141107T172645Z
+LOCATION:Mercury
+ORGANIZER;CN=User 02:urn:x-uid:user02
+SEQUENCE:0
+SUMMARY:Other organizer
+TRANSP:OPAQUE
+END:VEVENT
 END:VCALENDAR
 """
 
 
+# """BEGIN:VCALENDAR
+# VERSION:2.0
+# PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+# X-WR-CALNAME:calendar
+# BEGIN:VEVENT
+# UID:431C4ADB-4EB3-47F6-A18B-29441E329948
+# DTSTART;TZID=America/Los_Angeles:20141115T074500
+# DTEND;TZID=America/Los_Angeles:20141115T091500
+# ATTENDEE;CN=Cyrus Daboo;CUTYPE=INDIVIDUAL;EMAIL=cdaboo at apple.com;RSVP=TRUE
+#  ;SCHEDULE-STATUS=1.2:urn:x-uid:1D8B3915-4CA4-487E-B3CC-4822D80DFC97
+# ATTENDEE;CN=Morgen Sagen;CUTYPE=INDIVIDUAL;EMAIL=sagen at apple.com;PARTSTAT=
+#  ACCEPTED;ROLE=CHAIR:urn:x-uid:D0B38B00-4166-11DD-B22C-A07C87F02F6A
+# CREATED:20141112T215051Z
+# DTSTAMP:20141112T215143Z
+# ORGANIZER;CN=Morgen Sagen;EMAIL=sagen at apple.com:urn:x-uid:D0B38B00-4166-11
+#  DD-B22C-A07C87F02F6A
+# RRULE:FREQ=DAILY;COUNT=20
+# SEQUENCE:1
+# SUMMARY:Repeating
+# TRANSP:OPAQUE
+# END:VEVENT
+# BEGIN:VEVENT
+# UID:431C4ADB-4EB3-47F6-A18B-29441E329948
+# RECURRENCE-ID;TZID=America/Los_Angeles:20141117T074500
+# DTSTART;TZID=America/Los_Angeles:20141117T093000
+# DTEND;TZID=America/Los_Angeles:20141117T110000
+# ATTENDEE;CN=Cyrus Daboo;CUTYPE=INDIVIDUAL;EMAIL=cdaboo at apple.com;PARTSTAT=
+#  NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:1D8B3915-4CA4-487E-B
+#  3CC-4822D80DFC97
+# ATTENDEE;CN=Morgen Sagen;CUTYPE=INDIVIDUAL;EMAIL=sagen at apple.com;PARTSTAT=
+#  ACCEPTED;ROLE=CHAIR:urn:x-uid:D0B38B00-4166-11DD-B22C-A07C87F02F6A
+# CREATED:20141112T215051Z
+# DTSTAMP:20141112T215143Z
+# ORGANIZER;CN=Morgen Sagen;EMAIL=sagen at apple.com:urn:x-uid:D0B38B00-4166-11
+#  DD-B22C-A07C87F02F6A
+# SEQUENCE:1
+# SUMMARY:Repeating
+# TRANSP:OPAQUE
+# END:VEVENT
+# END:VCALENDAR
+# """
+
+
 class ImportTests(StoreTestCase):
     """
     Tests for importing data to a live store.
@@ -376,23 +517,64 @@
         )
         yield txn.commit()
 
-        # When importing the event again, instead trigger a re-invite
-        # from the organizer
+        # When importing the event again, update through the organizer's copy
+        # of the event as if it were an iTIP reply
         component = Component.allFromString(DATA_USER02_INVITES_USER01_ATTENDEE_COPY)
         yield importCollectionComponent(self.store, component)
 
         yield JobItem.waitEmpty(self.store.newTransaction, reactor, 60)
 
-        # Make sure attendee now has a new invite
+        # Make sure organizer now sees the right partstats
         txn = self.store.newTransaction()
+        home = yield txn.calendarHomeWithUID("user02")
+        collection = yield home.childWithName("calendar")
+        objects = yield collection.objectResources()
+        self.assertEquals(len(objects), 1)
+        component = yield objects[0].component()
+        # print(str(component))
+        props = component.getAttendeeProperties(("urn:x-uid:user01",))
+        # The master is ACCEPTED
+        self.assertEquals(
+            props[0].parameterValue("PARTSTAT"),
+            "ACCEPTED"
+        )
+        # 2nd instance is TENTATIVE
+        self.assertEquals(
+            props[1].parameterValue("PARTSTAT"),
+            "TENTATIVE"
+        )
+        # 3rd instance is not in the attendee's copy, so remains DECLILNED
+        self.assertEquals(
+            props[2].parameterValue("PARTSTAT"),
+            "DECLINED"
+        )
+        yield txn.commit()
+
+        # Make sure attendee now sees the right partstats
+        txn = self.store.newTransaction()
         home = yield txn.calendarHomeWithUID("user01")
         collection = yield home.childWithName("calendar")
         objects = yield collection.objectResources()
         self.assertEquals(len(objects), 1)
         component = yield objects[0].component()
-        prop = component.getAttendeeProperty(("urn:x-uid:user01",))
+        # print(str(component))
+        props = component.getAttendeeProperties(("urn:x-uid:user01",))
+        # The master is ACCEPTED
         self.assertEquals(
-            prop.parameterValue("PARTSTAT"),
-            "NEEDS-ACTION"
+            props[0].parameterValue("PARTSTAT"),
+            "ACCEPTED"
         )
+        # 2nd instance is TENTATIVE
+        self.assertEquals(
+            props[1].parameterValue("PARTSTAT"),
+            "TENTATIVE"
+        )
+        # 3rd instance is not in the organizer's copy, so should inherit
+        # the value from the master, which is ACCEPTED
+        self.assertEquals(
+            props[2].parameterValue("PARTSTAT"),
+            "ACCEPTED"
+        )
         yield txn.commit()
+
+    test_ImportComponentAttendee.todo = "Need to fix iTip reply processing"

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py	2014-11-13 19:57:52 UTC (rev 14165)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py	2014-11-13 20:05:54 UTC (rev 14166)
@@ -342,7 +342,7 @@
             C{tuple} of change info
         """
 
-        assert itip_message.propertyValue("METHOD") == "REPLY", "iTIP message must have METHOD:REPLY"
+        assert itip_message.propertyValue("METHOD") in ("REPLY", "X-RESTORE"), "iTIP message must have METHOD:REPLY"
         assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
 
         # Take each component in the reply and update the corresponding component
@@ -986,12 +986,12 @@
 
 
     @staticmethod
-    def generateAttendeeReply(original, attendee, changedRids=None, force_decline=False):
+    def generateAttendeeReply(original, attendee, changedRids=None, force_decline=False, method="REPLY"):
 
         # Start with a copy of the original as we may have to modify bits of it
         itip = original.duplicate()
         itip.replaceProperty(Property("PRODID", iCalendarProductID))
-        itip.addProperty(Property("METHOD", "REPLY"))
+        itip.addProperty(Property("METHOD", method))
 
         # Now filter out components except the ones specified
         itip.filterComponents(changedRids)

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2014-11-13 19:57:52 UTC (rev 14165)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2014-11-13 20:05:54 UTC (rev 14166)
@@ -147,7 +147,7 @@
 
 
     def isOrganizerReceivingMessage(self):
-        return self.method in ("REPLY", "REFRESH")
+        return self.method in ("REPLY", "REFRESH", "X-RESTORE")
 
 
     def isAttendeeReceivingMessage(self):
@@ -181,7 +181,7 @@
             returnValue((True, True, False, None,))
 
         # Handle new items differently than existing ones.
-        if self.method == "REPLY":
+        if self.method in ("REPLY", "X-RESTORE"):
             result = (yield self.doImplicitOrganizerUpdate())
         elif self.method == "REFRESH":
             # With implicit we ignore refreshes.
@@ -213,33 +213,40 @@
 
             organizer = self.recipient_calendar.getOrganizer()
 
-            # Build the schedule-changes XML element
             attendeeReplying, rids = processed
-            partstatChanged = False
-            reply_details = (customxml.Attendee.fromString(attendeeReplying),)
 
-            for rid, partstatChanged, privateCommentChanged in sorted(rids):
-                recurrence = []
-                if rid == "":
-                    recurrence.append(customxml.Master())
-                else:
-                    recurrence.append(customxml.RecurrenceID.fromString(rid))
-                changes = []
-                if partstatChanged:
-                    changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name="PARTSTAT"), name="ATTENDEE"))
-                    partstatChanged = True
-                if privateCommentChanged:
-                    changes.append(customxml.ChangedProperty(name="X-CALENDARSERVER-PRIVATE-COMMENT"))
-                recurrence.append(customxml.Changes(*changes))
-                reply_details += (customxml.Recurrence(*recurrence),)
+            if self.method == "X-RESTORE":
+                changes = None
+                partstatChanged = True
+                yield self._doRefresh(self.organizer_calendar_resource, only_attendees=(attendeeReplying,))
 
-            changes = customxml.ScheduleChanges(
-                customxml.DTStamp(),
-                customxml.Action(
-                    customxml.Reply(*reply_details),
-                ),
-            )
+            else:
+                # Build the schedule-changes XML element
+                partstatChanged = False
+                reply_details = (customxml.Attendee.fromString(attendeeReplying),)
 
+                for rid, partstatChanged, privateCommentChanged in sorted(rids):
+                    recurrence = []
+                    if rid == "":
+                        recurrence.append(customxml.Master())
+                    else:
+                        recurrence.append(customxml.RecurrenceID.fromString(rid))
+                    changes = []
+                    if partstatChanged:
+                        changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name="PARTSTAT"), name="ATTENDEE"))
+                        partstatChanged = True
+                    if privateCommentChanged:
+                        changes.append(customxml.ChangedProperty(name="X-CALENDARSERVER-PRIVATE-COMMENT"))
+                    recurrence.append(customxml.Changes(*changes))
+                    reply_details += (customxml.Recurrence(*recurrence),)
+
+                changes = customxml.ScheduleChanges(
+                    customxml.DTStamp(),
+                    customxml.Action(
+                        customxml.Reply(*reply_details),
+                    ),
+                )
+
             # Only update other attendees when the partstat was changed by the reply,
             # and only if the request does not indicate we should skip attendee refresh
             # (e.g. inbox item processing during migration from non-implicit server)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20141113/de5c37b8/attachment-0001.html>


More information about the calendarserver-changes mailing list