[CalendarServer-changes] [14302] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Jan 14 07:42:38 PST 2015


Revision: 14302
          http://trac.calendarserver.org//changeset/14302
Author:   cdaboo at apple.com
Date:     2015-01-14 07:42:38 -0800 (Wed, 14 Jan 2015)
Log Message:
-----------
Update to latest VPOLL spec.

Modified Paths:
--------------
    CalendarServer/trunk/contrib/webpoll/Makefile
    CalendarServer/trunk/contrib/webpoll/webapp/index.html
    CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js
    CalendarServer/trunk/requirements-dev.txt
    CalendarServer/trunk/requirements-stable.txt
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/simpleresource.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py

Property Changed:
----------------
    CalendarServer/trunk/contrib/webpoll/webapp/css/
    CalendarServer/trunk/contrib/webpoll/webapp/js/

Modified: CalendarServer/trunk/contrib/webpoll/Makefile
===================================================================
--- CalendarServer/trunk/contrib/webpoll/Makefile	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/contrib/webpoll/Makefile	2015-01-14 15:42:38 UTC (rev 14302)
@@ -15,22 +15,19 @@
 ##
 
 webpoll:
-	curl http://code.jquery.com/jquery-2.0.3.js -o webapp/js/jquery-2.0.3.js
-	curl http://code.jquery.com/ui/1.10.3/jquery-ui.js -o webapp/js/jquery-ui-1.10.3.js
-	curl https://raw.github.com/douglascrockford/JSON-js/master/json2.js -o webapp/js/json2.js
-	curl http://trentrichardson.com/examples/timepicker/jquery-ui-timepicker-addon.js -o webapp/js/datetimepicker.js
+	mkdir -p webapp/js/3rdparty
+	curl http://code.jquery.com/jquery-2.1.3.js -o webapp/js/3rdparty/jquery-2.1.3.js
+	curl http://code.jquery.com/ui/1.11.2/jquery-ui.js -o webapp/js/3rdparty/jquery-ui-1.11.2.js
+	curl -L https://github.com/douglascrockford/JSON-js/raw/master/json2.js -o webapp/js/3rdparty/json2.js
+	curl http://trentrichardson.com/examples/timepicker/jquery-ui-timepicker-addon.js -o webapp/js/3rdparty/datetimepicker.js
 	
-	curl http://trentrichardson.com/examples/timepicker/jquery-ui-timepicker-addon.css -o webapp/css/datetimepicker.css
-	curl http://jqueryui.com/resources/download/jquery-ui-themes-1.10.3.zip -o /tmp/jquery-ui-themes-1.10.3.zip
-	unzip /tmp/jquery-ui-themes-1.10.3.zip jquery-ui-themes-1.10.3/themes/cupertino/* -d /tmp
-	mv /tmp/jquery-ui-themes-1.10.3/themes/cupertino webapp/css
-	rm -rf /tmp/jquery-ui-themes-1.10.3
+	mkdir -p webapp/css/3rdparty
+	curl http://trentrichardson.com/examples/timepicker/jquery-ui-timepicker-addon.css -o webapp/css/3rdparty/datetimepicker.css
+	curl http://jqueryui.com/resources/download/jquery-ui-themes-1.11.2.zip -o /tmp/jquery-ui-themes-1.11.2.zip
+	unzip /tmp/jquery-ui-themes-1.11.2.zip jquery-ui-themes-1.11.2/themes/cupertino/* -d /tmp
+	mv /tmp/jquery-ui-themes-1.11.2/themes/cupertino webapp/css/3rdparty
+	rm -rf /tmp/jquery-ui-themes-1.11.2
 
 clean:
-	rm -f webapp/js/jquery-2.0.3.js
-	rm -f webapp/js/jquery-ui-1.10.3.js
-	rm -f webapp/js/json2.js
-	rm -f webapp/js/datetimepicker.js
-	
-	rm -f webapp/css/datetimepicker.css
-	rm -rf webapp/css/cupertino
+	rm -rf webapp/js/3rdparty
+	rm -rf webapp/css/3rdparty


Property changes on: CalendarServer/trunk/contrib/webpoll/webapp/css
___________________________________________________________________
Added: svn:ignore
   + 3rdparty


Modified: CalendarServer/trunk/contrib/webpoll/webapp/index.html
===================================================================
--- CalendarServer/trunk/contrib/webpoll/webapp/index.html	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/contrib/webpoll/webapp/index.html	2015-01-14 15:42:38 UTC (rev 14302)
@@ -3,13 +3,19 @@
 <head>
 	<meta charset="utf-8">
 	<title>WebPoll</title>
-	<link href="css/cupertino/jquery-ui.css" rel="stylesheet">
-	<link href="css/datetimepicker.css" rel="stylesheet">
+
+	<!-- 3rd party -->
+	<link href="css/3rdparty/cupertino/jquery-ui.css" rel="stylesheet">
+	<link href="css/3rdparty/datetimepicker.css" rel="stylesheet">
+	
+	<script src="js/3rdparty/jquery-2.1.3.js"></script>
+	<script src="js/3rdparty/jquery-ui-1.11.2.js"></script>
+	<script src="js/3rdparty/datetimepicker.js"></script>
+	<script src="js/3rdparty/json2.js"></script>
+	
+	<!-- Ours -->
 	<link href="css/webpoll.css" rel="stylesheet">
-	<script src="js/jquery-2.0.3.js"></script>
-	<script src="js/jquery-ui-1.10.3.js"></script>
-	<script src="js/datetimepicker.js"></script>
-	<script src="js/json2.js"></script>
+
 	<script src="js/utils.js"></script>
 	<script src="js/jcal.js"></script>
 	<script src="js/caldav.js"></script>


Property changes on: CalendarServer/trunk/contrib/webpoll/webapp/js
___________________________________________________________________
Added: svn:ignore
   + 3rdparty


Modified: CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js
===================================================================
--- CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/contrib/webpoll/webapp/js/caldav.js	2015-01-14 15:42:38 UTC (rev 14302)
@@ -772,34 +772,45 @@
 
 CalendarComponent.prototype.voter_responses = function() {
 	var voter_results = {}
-	$.each(this.data.properties("voter"), function(index, voter) {
-		voter_results[voter[3]] = parseInt(voter[1]["response"]);
+	var pollitemid = this.pollitemid();
+	$.each(this.parent.data.components("vvoter"), function(index, vvoter) {
+		var voter = vvoter.getPropertyValue("voter")
+		$.each(vvoter.components("vote"), function(index, vote) {
+			if (vote.getPropertyValue("poll-item-id") == pollitemid) {
+				voter_results[voter] = vote.getPropertyValue("response");
+				return false;
+			}
+		});
 	});
 	return voter_results;
 }
 
 // Change active user's response to this event
 CalendarComponent.prototype.changeVoterResponse = function(response) {
+	var matches_vvoter = $.grep(this.parent.data.components("vvoter"), function(vvoter, index) {
+		return gSession.currentPrincipal.matchingAddress(vvoter.getPropertyValue("voter"));
+	});
+	var pollitemid = this.pollitemid();
 	if (response !== null) {
-		var matches = $.grep(this.data.properties("voter"), function(voter, index) {
-			return gSession.currentPrincipal.matchingAddress(voter[3]);
+		var matches_vote = $.grep(matches_vvoter[0].components("vote"), function(vote, index) {
+			return vote.getPropertyValue("poll-item-id") == pollitemid;
 		});
-		if (matches.length == 1) {
-			new CalendarUser(matches[0], this).response(response.toString());
+		if (matches_vote.length == 1) {
+			matches_vote[0].getProperty("response")[3] = response;
 		} else {
-			this.data.newProperty(
-				"voter",
-				gSession.currentPrincipal.defaultAddress(),
-				{ "response" : response.toString() },
-				"cal-address"
-			);
-			this.changed(true);
+			var vote = matches_vvoter[0].newComponent("vote");
+			vote.newProperty("response", response, {}, "integer");
+			vote.newProperty("poll-item-id", pollitemid, {}, "integer");
 		}
 	} else {
-		this.data.removePropertiesMatchingValue(function(propdata) {
-			return propdata[0] == "voter" && gSession.currentPrincipal.matchingAddress(propdata[3]); 
+		$.each(matches_vvoter[0].components("vote"), function(index, vote) {
+			if (vote.getPropertyValue("poll-item-id") == pollitemid) {
+				matches_vvoter[0].caldata[2].remove(index);
+				return false;
+			}
 		});
 	}
+	this.changed(true);
 }
 
 // A container class for VCALENDAR objects
@@ -849,7 +860,8 @@
 		{ "cn" : gSession.currentPrincipal.cn },
 		"cal-address"
 	);
-	vpoll.newProperty(
+	var vvoter = vpoll.newComponent("vvoter");
+	vvoter.newProperty(
 		"voter",
 		gSession.currentPrincipal.defaultAddress(),
 		{
@@ -895,43 +907,55 @@
 // Add a new VEVENT to the VPOLL
 CalendarPoll.prototype.addEvent = function(dtstart, dtend) {
 	this.changed(true);
+	var poll_item_id = this.data.components("vevent").length;
 	var vevent = this.data.newComponent("vevent", true);
 	vevent.newProperty("dtstart", jcaldate.jsDateTojCal(dtstart), {}, "date-time");
 	vevent.newProperty("dtend", jcaldate.jsDateTojCal(dtend), {}, "date-time");
 	vevent.newProperty("summary", this.summary());
-	vevent.newProperty("poll-item-id", (this.data.components("vevent").length).toString());
-	vevent.newProperty(
-		"voter",
-		this.organizer(),
-		{"response" : "80"},
-		"cal-address"
-	);
+	vevent.newProperty("poll-item-id", poll_item_id, {}, "integer");
+
+	var matches_vvoter = $.grep(this.data.components("vvoter"), function(vvoter, index) {
+		return gSession.currentPrincipal.matchingAddress(vvoter.getPropertyValue("voter"));
+	});
+
+	var vvoter = null;
+	if (matches_vvoter.length == 1) {
+		vvoter = matches_vvoter[0];
+	}
+	else {
+		vvoter = this.data.newComponent("vvoter");
+		vvoter.newProperty("voter", this.organizer(), {}, "cal-address");
+	}
+	var vote = vvoter.newComponent("vote");
+	vote.newProperty("response", 80, {}, "integer");
+	vote.newProperty("poll-item-id", poll_item_id, {}, "integer");
 	return new CalendarEvent(vevent, this);
 }
 
 // Get an array of voters in the VPOLL
 CalendarPoll.prototype.voters = function() {
 	var this_vpoll = this;
-	return $.map(this.data.properties("voter"), function(voter) {
-		return new CalendarUser(voter, this_vpoll);
+	return $.map(this.data.components("vvoter"), function(vvoter) {
+		return new CalendarUser(vvoter.getProperty("voter"), this_vpoll);
 	});
 }
 
 // Add a voter to the VPOLL
 CalendarPoll.prototype.addVoter = function() {
 	this.changed(true);
-	return new CalendarUser(this.data.newProperty("voter", "", {}, "cal-address"), this);
+	var vvoter = this.data.newComponent("vvoter");
+	return new CalendarUser(vvoter.newProperty("voter", "", {}, "cal-address"), this);
 }
 
 // Mark current user as accepted
 CalendarPoll.prototype.acceptInvite = function() {
 	if (!this.isOwned()) {
-		var voters = $.grep(this.data.properties("voter"), function(voter) {
-			return gSession.currentPrincipal.matchingAddress(voter[3]);
-		});
-		$.each(voters, function(index, voter) {
-			voter[1]["partstat"] = "ACCEPTED";
-			delete voter[1]["rsvp"];
+		$.each(this.data.components("vvoter"), function(index, vvoter) {
+			var voter = vvoter.getProperty("voter");
+			if (gSession.currentPrincipal.matchingAddress(voter[3])) {
+				voter[1]["partstat"] = "ACCEPTED";
+				delete voter[1]["rsvp"];
+			}
 		})
 	}
 }
@@ -962,7 +986,8 @@
 	vevent.copyProperty("dtstart", this.data);
 	vevent.copyProperty("dtend", this.data);
 	vevent.copyProperty("organizer", vpoll.data);
-	$.each(vpoll.data.properties("voter"), function(index, voter) {
+	$.each(vpoll.data.components("vvoter"), function(index, vvoter) {
+		var voter = vvoter.getProperty("voter");
 		var attendee = vevent.newProperty(
 			"attendee",
 			voter[3],

Modified: CalendarServer/trunk/requirements-dev.txt
===================================================================
--- CalendarServer/trunk/requirements-dev.txt	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/requirements-dev.txt	2015-01-14 15:42:38 UTC (rev 14302)
@@ -8,4 +8,4 @@
 q
 tl.eggdeps
 --editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVClientLibrary/trunk@13420#egg=CalDAVClientLibrary
---editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@14244#egg=CalDAVTester
+--editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@14300#egg=CalDAVTester

Modified: CalendarServer/trunk/requirements-stable.txt
===================================================================
--- CalendarServer/trunk/requirements-stable.txt	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/requirements-stable.txt	2015-01-14 15:42:38 UTC (rev 14302)
@@ -68,7 +68,7 @@
 
     -e svn+http://svn.calendarserver.org/repository/calendarserver/PyKerberos/trunk@13420#egg=kerberos
 
-    -e svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@14245#egg=pycalendar
+    -e svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@14301#egg=pycalendar
     python-dateutil==1.5  # Note: v2.0+ is for Python 3
     pytz==2014.10
 

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -2296,6 +2296,25 @@
         return ()
 
 
+    def getOrganizerProperties(self):
+        """
+        Get the organizer value. Works on either a VCALENDAR or on a component.
+
+        @return: the string value of the Organizer property, or None
+        """
+
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            return [component.getOrganizerProperty() for component in self.subcomponents(ignore=True)]
+        else:
+            try:
+                return self.getProperty("ORGANIZER")
+            except InvalidICalendarDataError:
+                pass
+
+        return None
+
+
     def getOrganizerProperty(self):
         """
         Get the organizer value. Works on either a VCALENDAR or on a component.
@@ -2349,28 +2368,44 @@
 
 
     def recipientPropertyName(self):
-        return "VOTER" if self.name() == "VPOLL" else "ATTENDEE"
+        return "VOTER" if self.name() in ("VPOLL", "VVOTER",) else "ATTENDEE"
 
 
-    def getAttendees(self):
+    def getRecipientProperties(self):
         """
-        Get the attendee value. Works on either a VCALENDAR or on a component.
+        Get the attendee properties. Works on either a VCALENDAR or on a component.
 
-        @param match: a C{list} of calendar user address strings to try and match.
-        @return: a C{list} of the string values of the Attendee property, or None
+        @return: a C{list} of the the Attendee properties
         """
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents(ignore=True):
-                return component.getAttendees()
+                return component.getRecipientProperties()
         else:
             # Find the property values
-            return [p.value() for p in self.properties(self.recipientPropertyName())]
+            if self.name() == "VPOLL":
+                results = []
+                for c in self.subcomponents():
+                    if c.name() == "VVOTER":
+                        results.extend(c.properties(self.recipientPropertyName()))
+                return results
+            else:
+                return list(self.properties(self.recipientPropertyName()))
 
         return None
 
 
+    def getAttendees(self):
+        """
+        Get the attendee value. Works on either a VCALENDAR or on a component.
+
+        @return: a C{list} of the string values of the Attendee property, or None
+        """
+
+        return [p.value() for p in self.getRecipientProperties()]
+
+
     def getAttendeesByInstance(self, makeUnique=False, onlyScheduleAgentServer=False):
         """
         Get the attendee values for each instance. Optionally remove duplicates.
@@ -2392,7 +2427,7 @@
             result = ()
             attendees = set()
             rid = self.getRecurrenceIDUTC()
-            for attendee in tuple(self.properties(self.recipientPropertyName())):
+            for attendee in tuple(self.getRecipientProperties()):
 
                 if onlyScheduleAgentServer:
                     if attendee.hasParameter("SCHEDULE-AGENT"):
@@ -2450,7 +2485,7 @@
                     return attendee
         else:
             # Find the primary subcomponent
-            for attendee in self.properties(self.recipientPropertyName()):
+            for attendee in self.getRecipientProperties():
                 if normalizeCUAddr(attendee.value()) in test:
                     return attendee
 
@@ -2491,7 +2526,7 @@
                     yield attendee
         else:
             # Find the primary subcomponent
-            for attendee in self.properties(self.recipientPropertyName()):
+            for attendee in self.getRecipientProperties():
                 yield attendee
 
 
@@ -2845,6 +2880,33 @@
                 master_component.addProperty(Property("EXDATE", [exdate, ]))
 
 
+    def voterComponentForVoter(self, voter):
+        """
+        Find the VVOTER subcomponent with a VOTER property matching the specified attendee (voter).
+
+        @param voter: the calendar user address of the attendee (voter) to match
+        @type voter: L{str}
+        """
+        for voterComponent in tuple(self.subcomponents(ignore=True)):
+            if voterComponent.name() == "VVOTER" and voterComponent.getVoterProperty((voter,)) is not None:
+                return voterComponent
+        else:
+            return None
+
+
+    def voteMap(self):
+        """
+        Get a dict mapping each VOTE component POLL-ITEM-ID to the VOTE component.
+        """
+        results = {}
+        for component in self.subcomponents():
+            if component.name() == "VOTE":
+                poll_id = component.propertyValue("POLL-ITEM-ID")
+                if poll_id is not None:
+                    results[poll_id] = component
+        return results
+
+
     def filterComponents(self, rids):
 
         # If master is in rids do nothing
@@ -2873,7 +2935,15 @@
         assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
 
         for component in self.subcomponents(ignore=True):
-            [component.removeProperty(p) for p in tuple(component.properties(component.recipientPropertyName())) if p.value().lower() != attendee.lower()]
+            if component.name() == "VPOLL":
+                for vvoter in tuple(self.subcomponents()):
+                    if vvoter.name() == "VVOTER":
+                        if vvoter.propertyValue(component.recipientPropertyName()).lower() != attendee.lower():
+                            component.removeComponent(vvoter)
+            else:
+                for p in tuple(component.properties(component.recipientPropertyName())):
+                    if p.value().lower() != attendee.lower():
+                        component.removeProperty(p)
 
 
     def removeAllButTheseAttendees(self, attendees):
@@ -2886,7 +2956,15 @@
         attendees = set([attendee.lower() for attendee in attendees])
 
         for component in self.subcomponents(ignore=True):
-            [component.removeProperty(p) for p in tuple(component.properties(component.recipientPropertyName())) if p.value().lower() not in attendees]
+            if component.name() == "VPOLL":
+                for vvoter in tuple(self.subcomponents()):
+                    if vvoter.name() == "VVOTER":
+                        if vvoter.propertyValue(component.recipientPropertyName()).lower() not in attendees:
+                            component.removeComponent(vvoter)
+            else:
+                for p in tuple(component.properties(component.recipientPropertyName())):
+                    if p.value().lower() not in attendees:
+                        component.removeProperty(p)
 
 
     def hasAlarm(self):
@@ -3059,6 +3137,9 @@
             for prop in props:
                 for param in params:
                     prop.removeParameter(param)
+            if self.name() == "VPOLL":
+                for component in self.subcomponents(ignore=True):
+                    component.removePropertyParameters(property, params)
 
 
     def removePropertyParametersByValue(self, property, paramvalues):
@@ -3074,6 +3155,9 @@
             for prop in props:
                 for param, value in paramvalues:
                     prop.removeParameterValue(param, value)
+            if self.name() == "VPOLL":
+                for component in self.subcomponents(ignore=True):
+                    component.removePropertyParametersByValue(property, paramvalues)
 
 
     def getITIPInfo(self):

Modified: CalendarServer/trunk/twistedcaldav/simpleresource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/simpleresource.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/twistedcaldav/simpleresource.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -32,7 +32,6 @@
 
 from twisted.internet.defer import succeed
 
-from twistedcaldav.config import config
 from twistedcaldav.resource import CalDAVResource
 
 from txdav.xml import element as davxml
@@ -105,7 +104,7 @@
 
 
     def renderHTTP(self, request):
-        return http.RedirectResponse(request.unparseURL(host=config.ServerHostName, **self._kwargs))
+        return http.RedirectResponse(request.unparseURL(host=request.host, **self._kwargs))
 
 
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -593,36 +593,39 @@
 
         changed = False
 
-        # Get the VOTER properties in sub-components of the VPOLL as set by the attendee
-        poll_items = {}
-        for component in clientComponent.subcomponents():
-            poll_id = component.propertyValue("POLL-ITEM-ID")
-            if poll_id is not None:
-                poll_items[poll_id] = component.getVoterProperty((self.attendee,))
+        # Get the matching VVOTER component in each VPOLL
+        serverVoter = serverComponent.voterComponentForVoter(self.attendee)
+        clientVoter = clientComponent.voterComponentForVoter(self.attendee)
 
-        # Transfer attendee data with the master set
-        for component in serverComponent.subcomponents():
-            poll_id = component.propertyValue("POLL-ITEM-ID")
-            if poll_id is not None:
-                voter = component.getVoterProperty((self.attendee,))
-                attendee_voter = poll_items.get(poll_id)
-                if attendee_voter is None:
-                    if voter is not None:
-                        component.removeProperty(voter)
-                        changed = True
-                elif voter is None:
-                    component.addProperty(attendee_voter)
-                    changed = True
+        # Now get a map of each response
+        serverMap = serverVoter.voteMap()
+        clientMap = clientVoter.voteMap()
+
+        # Remove missing
+        for poll_id in set(serverMap.keys()) - set(clientMap.keys()):
+            serverVoter.removeComponent(serverMap[poll_id])
+            changed = True
+
+        # Add new ones
+        for poll_id in set(clientMap.keys()) - set(serverMap.keys()):
+            vote = clientMap[poll_id].duplicate()
+            vote.replaceProperty(Property("LAST-MODIFIED", DateTime.getNowUTC()))
+            serverVoter.addComponent(vote)
+            changed = True
+
+        # Look for response change
+        for poll_id in set(serverMap.keys()) & set(clientMap.keys()):
+            server_vote = serverMap[poll_id]
+            client_vote = clientMap[poll_id]
+            server_response = server_vote.propertyValue("RESPONSE")
+            client_response = client_vote.propertyValue("RESPONSE")
+            if server_response != client_response:
+                if client_response is not None:
+                    server_vote.replaceProperty(Property("RESPONSE", client_response))
                 else:
-                    for paramname in ("RESPONSE",):
-                        paramvalue = attendee_voter.parameterValue(paramname)
-                        if paramvalue is None:
-                            voter.removeParameter(paramname)
-                            changed = True
-                        else:
-                            if paramvalue != voter.parameterValue(paramname):
-                                voter.setParameter(paramname, paramvalue)
-                                changed = True
+                    server_vote.removeProperty("RESPONSE")
+                server_vote.replaceProperty(Property("LAST-MODIFIED", DateTime.getNowUTC()))
+                changed = True
 
         return changed
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -671,6 +671,8 @@
 
         self.coerceOrganizerScheduleAgent()
 
+        partstatProcessing = self.calendar.mainType() != "VPOLL"
+
         # Check for a delete
         if self.action == "remove":
 
@@ -692,8 +694,10 @@
                 self.oldcalendar = (yield self.resource.componentForUser())
             self.oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance(True, onlyScheduleAgentServer=True)
             self.oldInstances = set(self.oldcalendar.getComponentInstances())
-            self.coerceAttendeesPartstatOnModify()
 
+            if partstatProcessing:
+                self.coerceAttendeesPartstatOnModify()
+
             # Don't allow any SEQUENCE to decrease
             if self.oldcalendar and (not queued or not config.Scheduling.Options.WorkQueues.Enabled):
                 self.calendar.sequenceInSync(self.oldcalendar)
@@ -737,10 +741,11 @@
                                 # the PARTSTAT to NEEDS-ACTION.
                                 # The organizer is automatically ACCEPTED to the event.
                                 continue
-                            if attendee.hasParameter("PARTSTAT"):
-                                attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
-                            seq = comp.propertyValue("SEQUENCE", 0)
-                            attendee.setParameter("X-CALENDARSERVER-RESET-PARTSTAT", str(seq))
+                            if partstatProcessing:
+                                if attendee.hasParameter("PARTSTAT"):
+                                    attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
+                                seq = comp.propertyValue("SEQUENCE", 0)
+                                attendee.setParameter("X-CALENDARSERVER-RESET-PARTSTAT", str(seq))
 
                     # Look for changes to a specific attendee within an instance
                     for rid, attendees in needs_action_changed_rids.items():
@@ -750,10 +755,11 @@
                             if comp is not None:
                                 self.calendar.addComponent(comp)
 
-                        for attendee in comp.getAllAttendeeProperties():
-                            if attendee.value() in attendees:
-                                seq = comp.propertyValue("SEQUENCE", 0)
-                                attendee.setParameter("X-CALENDARSERVER-RESET-PARTSTAT", str(seq))
+                        if partstatProcessing:
+                            for attendee in comp.getAllAttendeeProperties():
+                                if attendee.value() in attendees:
+                                    seq = comp.propertyValue("SEQUENCE", 0)
+                                    attendee.setParameter("X-CALENDARSERVER-RESET-PARTSTAT", str(seq))
                 else:
                     log.debug("Implicit - organizer '{organizer}' is splitting UID: '{uid}'", organizer=self.organizer, uid=self.uid)
 
@@ -768,7 +774,8 @@
         elif self.action == "create":
             if self.split_details is None:
                 log.debug("Implicit - organizer '{organizer}' is creating UID: '{uid}'", organizer=self.organizer, uid=self.uid)
-                self.coerceAttendeesPartstatOnCreate()
+                if partstatProcessing:
+                    self.coerceAttendeesPartstatOnCreate()
 
                 # We need to handle the case where an organizer "restores" a previously delete event that has a sequence
                 # lower than the one used in the cancel that attendees may still have. In this case what we need to do
@@ -784,14 +791,15 @@
                 log.debug("Implicit - organizer '{organizer}' is creating a split UID: '{uid}'", organizer=self.organizer, uid=self.uid)
 
         # Always set RSVP=TRUE for any NEEDS-ACTION
-        for attendee in self.calendar.getAllAttendeeProperties():
-            if attendee.parameterValue("CUTYPE") != "X-SERVER-GROUP":
-                if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
-                    attendee.setParameter("RSVP", "TRUE")
-            else:
-                # Always remove RSVP and PARTSTAT
-                attendee.removeParameter("RSVP")
-                attendee.removeParameter("PARTSTAT")
+        if partstatProcessing:
+            for attendee in self.calendar.getAllAttendeeProperties():
+                if attendee.parameterValue("CUTYPE") != "X-SERVER-GROUP":
+                    if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
+                        attendee.setParameter("RSVP", "TRUE")
+                else:
+                    # Always remove RSVP and PARTSTAT
+                    attendee.removeParameter("RSVP")
+                    attendee.removeParameter("PARTSTAT")
 
         # If processing a queue item, actually execute the scheduling operations, else queue it.
         # Note a split is always a queued execution, so we do not need to re-queue
@@ -1229,6 +1237,9 @@
             aggregated.setdefault(attendee, []).append(rid)
 
         count = 0
+        recipientProperties = collections.defaultdict(list)
+        for p in self.calendar.getAllAttendeeProperties():
+            recipientProperties[p.value()].append(p)
         for attendee, rids in aggregated.iteritems():
 
             # Don't send message back to the ORGANIZER
@@ -1261,12 +1272,8 @@
 
                 if queued:
                     # Always make it look like scheduling succeeded when queuing
-                    self.calendar.setParameterToValueForPropertyWithValue(
-                        "SCHEDULE-STATUS",
-                        iTIPRequestStatus.MESSAGE_DELIVERED_CODE,
-                        "ATTENDEE",
-                        attendee,
-                    )
+                    for p in recipientProperties[attendee]:
+                        p.setParameter("SCHEDULE-STATUS", iTIPRequestStatus.MESSAGE_DELIVERED_CODE)
                 else:
                     # Add split details if needed
                     if self.split_details is not None:
@@ -1299,6 +1306,9 @@
 
         # Do one per attendee
         count = 0
+        recipientProperties = collections.defaultdict(list)
+        for p in self.calendar.getAllAttendeeProperties():
+            recipientProperties[p.value()].append(p)
         for attendee in self.attendees:
 
             # Don't send message back to the ORGANIZER
@@ -1327,10 +1337,8 @@
             # Do not schedule with groups - ever
             if attendeeAddress.hosted() and attendeeAddress.getCUType() == "GROUP":
                 # Set SCHEDULE-STATUS to something appropriate
-                self.calendar.setParametersForPropertyWithValue(
-                    {"SCHEDULE-STATUS": iTIPRequestStatus.REQUEST_FORWARDED_CODE if config.GroupAttendees.Enabled else iTIPRequestStatus.NO_USER_SUPPORT_CODE},
-                    "ATTENDEE", attendee,
-                )
+                for p in recipientProperties[attendee]:
+                    p.setParameter("SCHEDULE-STATUS", iTIPRequestStatus.REQUEST_FORWARDED_CODE if config.GroupAttendees.Enabled else iTIPRequestStatus.NO_USER_SUPPORT_CODE)
                 continue
 
             itipmsg = iTipGenerator.generateAttendeeRequest(self.calendar, (attendee,), self.changed_rids)
@@ -1340,12 +1348,8 @@
 
                 if queued:
                     # Always make it look like scheduling succeeded when queuing
-                    self.calendar.setParameterToValueForPropertyWithValue(
-                        "SCHEDULE-STATUS",
-                        iTIPRequestStatus.MESSAGE_DELIVERED_CODE,
-                        "ATTENDEE",
-                        attendee,
-                    )
+                    for p in recipientProperties[attendee]:
+                        p.setParameter("SCHEDULE-STATUS", iTIPRequestStatus.MESSAGE_DELIVERED_CODE)
                 else:
                     # Add split details if needed
                     if self.split_details is not None:
@@ -1411,20 +1415,19 @@
             self.queuedResponses.append(response)
         else:
             # Map each recipient in the response to a status code
+            recipients = collections.defaultdict(list)
+            for p in self.calendar.getAllAttendeeProperties() if is_organizer else self.calendar.getOrganizerProperties():
+                recipients[p.value()].append(p)
+
             responses = {}
-            propname = self.calendar.mainComponent().recipientPropertyName() if is_organizer else "ORGANIZER"
             for item in response.responses:
                 recipient = str(item.recipient.children[0])
                 status = str(item.reqstatus)
                 responses[recipient] = status
 
                 # Now apply to each ATTENDEE/ORGANIZER in the original data
-                self.calendar.setParameterToValueForPropertyWithValue(
-                    "SCHEDULE-STATUS",
-                    status.split(";")[0],
-                    propname,
-                    recipient,
-                )
+                for p in recipients[recipient]:
+                    p.setParameter("SCHEDULE-STATUS", status.split(";")[0])
 
 
     @inlineCallbacks

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -485,7 +485,7 @@
             reqstatus = "2.0"
 
         # Get attendee in reply_component - there MUST be only one
-        attendees = tuple(reply_component.properties(reply_component.recipientPropertyName()))
+        attendees = tuple(reply_component.getRecipientProperties())
         if len(attendees) != 1:
             log.error("There must be one and only one ATTENDEE property in a REPLY\n%s" % (str(reply_component),))
             return None, False, False
@@ -500,17 +500,18 @@
 
         # Only process the change for this component if it was made after the last partstat reset
         if existing_attendee and reply_sequence >= existing_reset_sequence:
-            oldpartstat = existing_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
-            existing_attendee.setParameter("PARTSTAT", partstat)
-            existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus)
-            partstat_changed = (oldpartstat != partstat)
+            if existing_attendee.name() == "ATTENDEE":
+                oldpartstat = existing_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
+                existing_attendee.setParameter("PARTSTAT", partstat)
+                existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus)
+                partstat_changed = (oldpartstat != partstat)
 
-            # Always delete RSVP on PARTSTAT change
-            if partstat_changed:
-                try:
-                    existing_attendee.removeParameter("RSVP")
-                except KeyError:
-                    pass
+                # Always delete RSVP on PARTSTAT change
+                if partstat_changed:
+                    try:
+                        existing_attendee.removeParameter("RSVP")
+                    except KeyError:
+                        pass
 
             # Handle attendee comments
             if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
@@ -583,7 +584,8 @@
     @staticmethod
     def updateVPOLLDataFromReply(reply_component, organizer_component, attendee):
         """
-        Update VPOLL sub-components with voter's response.
+        Update VPOLL sub-components with voter's response. Just replace the organizer's
+        VVOTER component for the replying attendee (voter) with the one in the replyVoter.
 
         @param reply_component: component to copy from
         @type reply_component: L{Component}
@@ -593,29 +595,47 @@
         @type attendee: L{Property}
         """
 
-        responses = {}
-        for prop in reply_component.properties("POLL-ITEM-ID"):
-            responses[prop.value()] = prop
+        # Get REQUEST-STATUS as we need to write that into the saved ATTENDEE property
+        reqstatus = tuple(reply_component.properties("REQUEST-STATUS"))
+        if reqstatus:
+            reqstatus = ",".join(status.value()[0] for status in reqstatus)
+        else:
+            reqstatus = "2.0"
 
-        for component in organizer_component.subcomponents(ignore=True):
-            poll_item_id = component.propertyValue("POLL-ITEM-ID")
-            if poll_item_id is None:
-                continue
-            voter = component.getVoterProperty((attendee.value(),))
+        # Get the matching VVOTER component in each VPOLL
+        replyVoter = reply_component.voterComponentForVoter(attendee.value())
+        organizerVoter = organizer_component.voterComponentForVoter(attendee.value())
 
-            # If no response - remove
-            if poll_item_id not in responses or not responses[poll_item_id].hasParameter("RESPONSE"):
-                if voter is not None:
-                    component.removeProperty(voter)
-                continue
+        if replyVoter is None:
+            return
 
-            # Add or update voter
-            if voter is None:
-                voter = Property("VOTER", attendee.value())
-                component.addProperty(voter)
-            voter.setParameter("RESPONSE", responses[poll_item_id].parameterValue("RESPONSE"))
+        if organizerVoter is None:
+            # Add in the new one
+            organizerVoter = replyVoter.duplicate()
+            reply_component.addComponent(organizerVoter)
+        else:
+            # Merge each vote
+            replyMap = replyVoter.voteMap()
+            organizerMap = organizerVoter.voteMap()
 
+            # Add new ones
+            for vote in set(replyMap.keys()) - set(organizerMap.keys()):
+                organizerVoter.addComponent(replyMap[vote].duplicate())
 
+            # Replace existing ones
+            for vote in set(replyMap.keys()) & set(organizerMap.keys()):
+                organizerVoter.removeComponent(organizerMap[vote])
+                organizerVoter.addComponent(replyMap[vote].duplicate())
+
+        # Update VOTER property
+        existing_voter = organizerVoter.getProperty("VOTER")
+        existing_voter.setParameter("SCHEDULE-STATUS", reqstatus)
+        try:
+            existing_voter.removeParameter("RSVP")
+        except KeyError:
+            pass
+
+
     @staticmethod
     def transferItems(from_calendar, to_component, needs_action_rids, reschedule, master_details, remove_matched=False):
         """
@@ -1068,22 +1088,18 @@
     @staticmethod
     def generateVPOLLReply(vpoll, attendee):
         """
-        Generate the proper poll response in a reply for each component being voted on.
+        Generate the proper poll response in a reply by removing all sub-components
+        except fore the VVOTER matching the attendee (voter) replying.
 
         @param vpoll: the VPOLL component to process
         @type vpoll: L{Component}
-        @param attendee: calendar user address of attendee replying
+        @param attendee: calendar user address of attendee (voter) replying
         @type attendee: C{str}
         """
 
         for component in tuple(vpoll.subcomponents(ignore=True)):
-            poll_item_id = component.propertyValue("POLL-ITEM-ID")
-            if poll_item_id is None:
-                continue
-            voter = component.getVoterProperty((attendee,))
-            if voter is not None and voter.hasParameter("RESPONSE"):
-                vpoll.addProperty(Property("POLL-ITEM-ID", poll_item_id, {"RESPONSE": voter.parameterValue("RESPONSE")}))
-            vpoll.removeComponent(component)
+            if component.name() != "VVOTER" or component.getVoterProperty((attendee,)) is None:
+                vpoll.removeComponent(component)
 
 
     @staticmethod

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -6615,3 +6615,740 @@
             strcal2 = str(cal2)
             strchanged = str(Component.fromString(changed_calendar))
             self.assertEqual(strchanged, strcal2, msg="%s mismatch:\n%s" % (description, "\n".join(unified_diff(strchanged.split("\n"), strcal2.split("\n")))))
+
+
+    def test_attendee_merge_vpoll(self):
+
+        data = (
+            (
+                "#1.1 Simple component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+DTSTAMP:20150113T152404Z
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.2 Simple component, response added",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+DTSTAMP:20150113T152404Z
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:XXXXXXXXTXXXXXXZ
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.3 Simple component, response changed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:20150113T152404Z
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+LAST-MODIFIED:20150113T152404Z
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+DTSTAMP:20150113T152404Z
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+LAST-MODIFIED:XXXXXXXXTXXXXXXZ
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.4 Simple component, response removed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:20150113T152404Z
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+DTSTAMP:20150113T152404Z
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.5 Simple component, response unchanged",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:20150113T152404Z
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:20150113T152404Z
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+DTSTAMP:20150113T152404Z
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:XXXXXXXXTXXXXXXZ
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.6 Simple component, bad changed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:20150113T152404Z
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 02":mailto:user2 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:20150113T152404Z
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+DTSTAMP:20150113T152404Z
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+LAST-MODIFIED:XXXXXXXXTXXXXXXZ
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""")
+            ),
+        )
+
+        for description, calendar1, calendar2, attendee, result in data:
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
+            diffResult = differ.attendeeMerge(attendee)
+            diffResult = (
+                diffResult[0],
+                diffResult[1],
+                tuple(sorted(diffResult[2])),
+                re.sub(
+                    "LAST-MODIFIED:.*",
+                    "LAST-MODIFIED:XXXXXXXXTXXXXXXZ",
+                    str(diffResult[3]).replace("\r", "").replace("\n ", "")
+                ) if diffResult[3] else None,
+            )
+            result = list(result)
+            result[2] = tuple([(DateTime.parseText(dt) if dt else None) for dt in result[2]])
+            result = tuple(result)
+            self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -1775,6 +1775,434 @@
 """,
                 True,
             ),
+            (
+                "3.1 Simple VPOLL Reply - response added",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+END:VVOTER
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;SCHEDULE-STATUS=2.0:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "3.2 Simple VPOLL Reply - response changed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+END:VOTE
+END:VVOTER
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;SCHEDULE-STATUS=2.0:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "3.3 Simple VPOLL Reply - response added and changed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;SCHEDULE-STATUS=2.0:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "3.4 Simple VPOLL Reply - response one changed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:0
+END:VOTE
+END:VVOTER
+END:VPOLL
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;SCHEDULE-STATUS=2.0:mailto:user2 at example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:0
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+                True,
+            ),
         )
 
         for title, calendar_txt, itip_txt, changed_txt, expected in data:

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py	2015-01-14 15:39:56 UTC (rev 14301)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py	2015-01-14 15:42:38 UTC (rev 14302)
@@ -32,6 +32,7 @@
 from txdav.common.datastore.sql_tables import schema, \
     scheduleActionToSQL, scheduleActionFromSQL
 
+import collections
 import datetime
 import hashlib
 import traceback
@@ -262,18 +263,19 @@
 
         # Map each recipient in the response to a status code
         changed = False
-        propname = calendar.mainComponent().recipientPropertyName() if is_organizer else "ORGANIZER"
+        recipients = collections.defaultdict(list)
+        for p in calendar.getAllAttendeeProperties() if is_organizer else calendar.getOrganizerProperties():
+            recipients[p.value()].append(p)
+
         for recipient, statusCode in response:
             # Now apply to each ATTENDEE/ORGANIZER in the original data only if not 1.2
             if statusCode != iTIPRequestStatus.MESSAGE_DELIVERED_CODE:
-                calendar.setParameterToValueForPropertyWithValue(
-                    "SCHEDULE-STATUS",
-                    statusCode,
-                    propname,
-                    recipient,
-                )
-                changed = True
 
+                # Now apply to each ATTENDEE/ORGANIZER in the original data
+                for p in recipients[recipient]:
+                    p.setParameter("SCHEDULE-STATUS", statusCode)
+                    changed = True
+
         return changed
 
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150114/67d8444a/attachment-0001.html>


More information about the calendarserver-changes mailing list