[prev in list] [next in list] [prev in thread] [next in thread]
List: calendarserver-changes
Subject: [CalendarServer-changes] [14353] CalendarServer/trunk/txdav/caldav/datastore/scheduling
From: source_changes () macosforge ! org
Date: 2015-01-29 22:44:58
Message-ID: 20150129224458.96DF7140F01 () svn ! calendarserver ! org
[Download RAW message or body]
[Attachment #2 (multipart/alternative)]
Revision: 14353
http://trac.calendarserver.org//changeset/14353
Author: cdaboo@apple.com
Date: 2015-01-29 14:44:58 -0800 (Thu, 29 Jan 2015)
Log Message:
-----------
VPOLL related updates.
Modified Paths:
--------------
CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py
CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py 2015-01-29 \
20:22:01 UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py 2015-01-29 \
22:44:58 UTC (rev 14353) @@ -358,6 +358,40 @@
@staticmethod
+ def processPollStatus(itip_message, calendar, recipient):
+ """
+ Process a METHOD=POLLSTATUS.
+
+ @param itip_message: the iTIP message to process.
+ @type itip_message: L{Component}
+ @param calendar: the calendar object to apply the POLLSTATUS to
+ @type calendar: L{Component}
+
+ @return: the update calendar component or C{None}
+ """
+
+ # Check sequencing
+ if not iTipProcessing.sequenceComparison(itip_message, calendar):
+ # Ignore out of sequence message
+ return None
+
+ calendar_master = calendar.masterComponent()
+ itip_master = itip_message.masterComponent()
+
+ # Remove each VVOTER in the original (except for the recipients)
+ for component in tuple(calendar_master.subcomponents()):
+ if component.name() == "VVOTER" and component.propertyValue("VOTER") != \
recipient: + calendar_master.removeComponent(component)
+
+ # Add each VVOTER in the iTip message
+ for component in itip_master.subcomponents():
+ if component.name() == "VVOTER" and component.propertyValue("VOTER") != \
recipient: + calendar_master.addComponent(component.duplicate())
+
+ return calendar
+
+
+ @staticmethod
def processReply(itip_message, calendar):
"""
Process a METHOD=REPLY.
@@ -576,7 +610,7 @@
# Do VPOLL transfer
if reply_component.name() == "VPOLL":
# TODO: figure out how to report changes back
- iTipProcessing.updateVPOLLDataFromReply(reply_component, \
organizer_component, attendee) + partstat_changed = \
iTipProcessing.updateVPOLLDataFromReply(reply_component, organizer_component, \
attendee)
return attendee.value(), partstat_changed, private_comment_changed
@@ -595,6 +629,8 @@
@type attendee: L{Property}
"""
+ partstat_changed = False
+
# Get REQUEST-STATUS as we need to write that into the saved ATTENDEE \
property reqstatus = tuple(reply_component.properties("REQUEST-STATUS"))
if reqstatus:
@@ -607,12 +643,13 @@
organizerVoter = \
organizer_component.voterComponentForVoter(attendee.value())
if replyVoter is None:
- return
+ return partstat_changed
if organizerVoter is None:
# Add in the new one
organizerVoter = replyVoter.duplicate()
reply_component.addComponent(organizerVoter)
+ partstat_changed = True
else:
# Merge each vote
replyMap = replyVoter.voteMap()
@@ -621,9 +658,12 @@
# Add new ones
for vote in set(replyMap.keys()) - set(organizerMap.keys()):
organizerVoter.addComponent(replyMap[vote].duplicate())
+ partstat_changed = True
# Replace existing ones
for vote in set(replyMap.keys()) & set(organizerMap.keys()):
+ if organizerMap[vote].propertyValue("RESPONSE") != \
replyMap[vote].propertyValue("RESPONSE"): + partstat_changed = \
True organizerVoter.removeComponent(organizerMap[vote])
organizerVoter.addComponent(replyMap[vote].duplicate())
@@ -635,7 +675,9 @@
except KeyError:
pass
+ return partstat_changed
+
@staticmethod
def transferItems(from_calendar, to_component, needs_action_rids, reschedule, \
master_details, remove_matched=False): """
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py 2015-01-29 \
20:22:01 UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py 2015-01-29 \
22:44:58 UTC (rev 14353) @@ -90,8 +90,9 @@
@param recipient: calendar user receiving the message
@type recipient: C{str}
- @return: a C{tuple} of (C{bool}, C{bool}) indicating whether the message was \
processed, and if it was whether
- auto-processing has taken place.
+ @return: a C{tuple} of (C{bool}, C{bool}, C{bool}, C{bool}) indicating \
whether the message was processed, + and if it was whether auto-processing \
has taken place, whether it needs to be stored in the inbox, and + the \
changes property for the inbox item. """
self.txn = txn
@@ -151,7 +152,7 @@
def isAttendeeReceivingMessage(self):
- return self.method in ("REQUEST", "ADD", "CANCEL")
+ return self.method in ("REQUEST", "ADD", "CANCEL", "POLLSTATUS")
@inlineCallbacks
@@ -393,6 +394,8 @@
elif self.method == "ADD":
# TODO: implement ADD
result = (False, False, False, None)
+ elif self.method == "POLLSTATUS":
+ result = (yield self.doImplicitAttendeePollStatus())
else:
# NB We should never get here as we will have rejected unsupported \
METHODs earlier. result = (True, True, False, None,)
@@ -552,7 +555,7 @@
else:
# Request needs to be ignored
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' \
processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, \
self.recipient.cuaddr, self.uid))
- result = (True, True, False, None,)
+ result = (True, False, False, None,)
returnValue(result)
@@ -570,7 +573,7 @@
# If there is no existing copy, then ignore
if self.recipient_calendar is None:
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' \
ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, \
self.recipient.cuaddr, self.uid))
- result = (True, True, True, None)
+ result = (True, False, True, None)
else:
# Need to check for auto-respond attendees. These need to suppress the \
inbox message
# if the cancel is processed. However, if the principal is a user we \
always force the @@ -631,12 +634,36 @@
result = (True, autoprocessed, store_inbox, changes)
else:
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' \
processing METHOD:CANCEL, UID: '%s' - ignoring" % (self.originator.cuaddr, \
self.recipient.cuaddr, self.uid))
- result = (True, True, False, None)
+ result = (True, False, False, None)
returnValue(result)
@inlineCallbacks
+ def doImplicitAttendeePollStatus(self):
+ """
+ An iTIP message status update has been sent to an attendee by the organizer. \
We need to update the + attendee state based on the nature of the iTIP \
message. + """
+ # If there is no existing copy, then we must fail
+ if self.new_resource:
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' \
processing METHOD:POLLSTATUS, UID: '%s' - attendee has no copy" % \
(self.originator.cuaddr, self.recipient.cuaddr, self.uid)) + \
returnValue((True, False, False, None,)) +
+ processed_message = iTipProcessing.processPollStatus(self.message, \
self.recipient_calendar) +
+ # Let the store know that no time-range info has changed for a refresh \
(assuming that + # no auto-accept changes were made)
+ processed_message.noInstanceIndexing = True
+
+ # Update the attendee's copy of the event
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing \
METHOD:POLLSTATUS, UID: '%s' - updating poll" % (self.originator.cuaddr, \
self.recipient.cuaddr, self.uid)) + yield self.writeCalendarResource(None, \
self.recipient_calendar_resource, processed_message) +
+ returnValue((True, False, False, None,))
+
+
+ @inlineCallbacks
def checkAttendeeAutoReply(self, calendar, automode):
"""
Check whether a reply to the given iTIP message is needed and if so make the
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py 2015-01-29 \
20:22:01 UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py 2015-01-29 \
22:44:58 UTC (rev 14353) @@ -515,7 +515,7 @@
yield self.generateRemoteSchedulingResponses(otherserver_recipients, \
responses, freebusy, getattr(self.txn, 'doing_attendee_refresh', False))
# To reduce chatter, we suppress certain messages
- if not self.suppress_refresh:
+ if not self.suppress_refresh or self.calendar.mainType() == "VPOLL":
# Now process remote recipients
if remote_recipients:
Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py 2015-01-29 \
20:22:01 UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py 2015-01-29 \
22:44:58 UTC (rev 14353) @@ -21,7 +21,7 @@
from twisted.trial import unittest
from twistedcaldav.stdconfig import config
-from twistedcaldav.ical import Component
+from twistedcaldav.ical import Component, normalize_iCalStr
from txdav.caldav.datastore.scheduling.itip import iTipProcessing, iTipGenerator
@@ -1289,6 +1289,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"1.2 Simple Reply - recurring no overrides",
@@ -1335,6 +1336,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"1.3 Simple Reply - recurring with missing master",
@@ -1389,6 +1391,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"1.4 Simple Reply - recurring with overrides in master but not \
reply", @@ -1453,6 +1456,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"1.5 Simple Reply - recurring with overrides in master invalid in \
reply", @@ -1517,6 +1521,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"1.6 Simple Reply - recurring with overrides in master, invalid ones \
in reply", @@ -1590,6 +1595,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"2.1 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
@@ -1636,6 +1642,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"2.2 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
@@ -1682,6 +1689,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"2.3 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
@@ -1728,6 +1736,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"2.4 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
@@ -1774,6 +1783,7 @@
END:VCALENDAR
""",
True,
+ None,
),
(
"3.1 Simple VPOLL Reply - response added",
@@ -1874,6 +1884,7 @@
END:VCALENDAR
""",
True,
+ ('mailto:user2@example.com', set([("", True, False,)])),
),
(
"3.2 Simple VPOLL Reply - response changed",
@@ -1978,6 +1989,7 @@
END:VCALENDAR
""",
True,
+ ('mailto:user2@example.com', set([("", True, False,)])),
),
(
"3.3 Simple VPOLL Reply - response added and changed",
@@ -2090,6 +2102,7 @@
END:VCALENDAR
""",
True,
+ ('mailto:user2@example.com', set([("", True, False,)])),
),
(
"3.4 Simple VPOLL Reply - response one changed",
@@ -2202,21 +2215,475 @@
END:VCALENDAR
""",
True,
+ ('mailto:user2@example.com', set([("", True, False,)])),
),
+ (
+ "3.5 Simple VPOLL Reply - no changes",
+ """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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@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@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;SCHEDULE-STATUS=2.0:mailto:user2@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@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+ True,
+ ('mailto:user2@example.com', set([])),
+ ),
)
- for title, calendar_txt, itip_txt, changed_txt, expected in data:
+ for title, calendar_txt, itip_txt, changed_txt, expected, processed in data:
calendar = Component.fromString(calendar_txt)
itip = Component.fromString(itip_txt)
if expected:
changed = Component.fromString(changed_txt)
- result, _ignore = iTipProcessing.processReply(itip, calendar)
+ result, result_processed = iTipProcessing.processReply(itip, calendar)
self.assertEqual(result, expected, msg="Result mismatch: %s" % (title,))
if expected:
self.assertEqual(changed, calendar, msg="Calendar mismatch: %s" % \
(title,)) + if processed is not None:
+ self.assertEqual(result_processed, processed, msg="Process mismatch: \
%s" % (title,))
+ def test_processPollStatus(self):
+ """
+ Test iTIPProcessing.processPollStatus
+ """
+
+ data = (
+ (
+ "3.1 Simple VPOLL - 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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@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:POLLSTATUS
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:0
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+ ),
+ (
+ "3.2 Simple VPOLL - recipient response not 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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@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:POLLSTATUS
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:0
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:0
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for title, calendar_txt, itip_txt, changed_txt in data:
+ calendar = Component.fromString(calendar_txt)
+ itip = Component.fromString(itip_txt)
+
+ result = iTipProcessing.processPollStatus(itip, calendar, \
"mailto:user2@example.com") + self.assertEqual(normalize_iCalStr(result), \
normalize_iCalStr(changed_txt), msg="Calendar mismatch: %s" % (title,)) +
+
def test_update_attendee_partstat(self):
data = (
[Attachment #5 (text/html)]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14353] CalendarServer/trunk/txdav/caldav/datastore/scheduling</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: \
verdana,arial,helvetica,sans-serif; font-size: 10pt; } #msg dl a { font-weight: \
bold} #msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: \
bold; } #msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: \
6px; } #logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em \
0; } #logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg \
h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; } \
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; \
} #logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: \
-1.5em; padding-left: 1.5em; } #logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em \
1em 0 1em; background: white;} #logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid \
#fa0; border-bottom: 1px solid #fa0; background: #fff; } #logmsg table th { \
text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted \
#fa0; } #logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: \
0.2em 0.5em; } #logmsg table thead th { text-align: center; border-bottom: 1px solid \
#fa0; } #logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: \
6px; } #patch { width: 100%; }
#patch h4 {font-family: \
verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, \
#patch .copfile {border:1px solid #ccc;margin:10px 0;} #patch ins \
{background:#dfd;text-decoration:none;display:block;padding:0 10px;} #patch del \
{background:#fdd;text-decoration:none;display:block;padding:0 10px;} #patch .lines, \
.info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a \
href="http://trac.calendarserver.org//changeset/14353">14353</a></dd> <dt>Author</dt> \
<dd>cdaboo@apple.com</dd> <dt>Date</dt> <dd>2015-01-29 14:44:58 -0800 (Thu, 29 Jan \
2015)</dd> </dl>
<h3>Log Message</h3>
<pre>VPOLL related updates.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingitippy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_itippy">CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingitippy"></a>
<div class="modfile"><h4>Modified: \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py (14352 => 14353)</h4> \
<pre class="diff"><span> <span class="info">--- \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py 2015-01-29 20:22:01 \
UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py 2015-01-29 \
22:44:58 UTC (rev 14353) </span><span class="lines">@@ -358,6 +358,40 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @staticmethod
</span><ins>+ def processPollStatus(itip_message, calendar, recipient):
+ """
+ Process a METHOD=POLLSTATUS.
+
+ @param itip_message: the iTIP message to process.
+ @type itip_message: L{Component}
+ @param calendar: the calendar object to apply the POLLSTATUS to
+ @type calendar: L{Component}
+
+ @return: the update calendar component or C{None}
+ """
+
+ # Check sequencing
+ if not iTipProcessing.sequenceComparison(itip_message, calendar):
+ # Ignore out of sequence message
+ return None
+
+ calendar_master = calendar.masterComponent()
+ itip_master = itip_message.masterComponent()
+
+ # Remove each VVOTER in the original (except for the recipients)
+ for component in tuple(calendar_master.subcomponents()):
+ if component.name() == "VVOTER" and \
component.propertyValue("VOTER") != recipient: + \
calendar_master.removeComponent(component) +
+ # Add each VVOTER in the iTip message
+ for component in itip_master.subcomponents():
+ if component.name() == "VVOTER" and \
component.propertyValue("VOTER") != recipient: + \
calendar_master.addComponent(component.duplicate()) +
+ return calendar
+
+
+ @staticmethod
</ins><span class="cx"> def processReply(itip_message, calendar):
</span><span class="cx"> """
</span><span class="cx"> Process a METHOD=REPLY.
</span><span class="lines">@@ -576,7 +610,7 @@
</span><span class="cx"> # Do VPOLL transfer
</span><span class="cx"> if reply_component.name() == "VPOLL":
</span><span class="cx"> # TODO: figure out how to report changes \
back </span><del>- \
iTipProcessing.updateVPOLLDataFromReply(reply_component, organizer_component, \
attendee) </del><ins>+ partstat_changed = \
iTipProcessing.updateVPOLLDataFromReply(reply_component, organizer_component, \
attendee) </ins><span class="cx">
</span><span class="cx"> return attendee.value(), partstat_changed, \
private_comment_changed </span><span class="cx">
</span><span class="lines">@@ -595,6 +629,8 @@
</span><span class="cx"> @type attendee: L{Property}
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+ partstat_changed = False
+
</ins><span class="cx"> # Get REQUEST-STATUS as we need to write that into \
the saved ATTENDEE property </span><span class="cx"> reqstatus = \
tuple(reply_component.properties("REQUEST-STATUS")) </span><span \
class="cx"> if reqstatus: </span><span class="lines">@@ -607,12 +643,13 @@
</span><span class="cx"> organizerVoter = \
organizer_component.voterComponentForVoter(attendee.value()) </span><span class="cx"> \
</span><span class="cx"> if replyVoter is None:
</span><del>- return
</del><ins>+ return partstat_changed
</ins><span class="cx">
</span><span class="cx"> if organizerVoter is None:
</span><span class="cx"> # Add in the new one
</span><span class="cx"> organizerVoter = replyVoter.duplicate()
</span><span class="cx"> reply_component.addComponent(organizerVoter)
</span><ins>+ partstat_changed = True
</ins><span class="cx"> else:
</span><span class="cx"> # Merge each vote
</span><span class="cx"> replyMap = replyVoter.voteMap()
</span><span class="lines">@@ -621,9 +658,12 @@
</span><span class="cx"> # Add new ones
</span><span class="cx"> for vote in set(replyMap.keys()) - \
set(organizerMap.keys()): </span><span class="cx"> \
organizerVoter.addComponent(replyMap[vote].duplicate()) </span><ins>+ \
partstat_changed = True </ins><span class="cx">
</span><span class="cx"> # Replace existing ones
</span><span class="cx"> for vote in set(replyMap.keys()) & \
set(organizerMap.keys()): </span><ins>+ if \
organizerMap[vote].propertyValue("RESPONSE") != \
replyMap[vote].propertyValue("RESPONSE"): + \
partstat_changed = True </ins><span class="cx"> \
organizerVoter.removeComponent(organizerMap[vote]) </span><span class="cx"> \
organizerVoter.addComponent(replyMap[vote].duplicate()) </span><span class="cx">
</span><span class="lines">@@ -635,7 +675,9 @@
</span><span class="cx"> except KeyError:
</span><span class="cx"> pass
</span><span class="cx">
</span><ins>+ return partstat_changed
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> @staticmethod
</span><span class="cx"> def transferItems(from_calendar, to_component, \
needs_action_rids, reschedule, master_details, remove_matched=False): </span><span \
class="cx"> """ </span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingprocessingpy"></a>
<div class="modfile"><h4>Modified: \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py (14352 => \
14353)</h4> <pre class="diff"><span>
<span class="info">--- \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py 2015-01-29 \
20:22:01 UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py 2015-01-29 \
22:44:58 UTC (rev 14353) </span><span class="lines">@@ -90,8 +90,9 @@
</span><span class="cx"> @param recipient: calendar user receiving the \
message </span><span class="cx"> @type recipient: C{str}
</span><span class="cx">
</span><del>- @return: a C{tuple} of (C{bool}, C{bool}) indicating whether the \
message was processed, and if it was whether
- auto-processing has taken place.
</del><ins>+ @return: a C{tuple} of (C{bool}, C{bool}, C{bool}, C{bool}) \
indicating whether the message was processed, + and if it was whether \
auto-processing has taken place, whether it needs to be stored in the inbox, and + \
the changes property for the inbox item. </ins><span class="cx"> \
""" </span><span class="cx">
</span><span class="cx"> self.txn = txn
</span><span class="lines">@@ -151,7 +152,7 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> def isAttendeeReceivingMessage(self):
</span><del>- return self.method in ("REQUEST", "ADD", \
"CANCEL") </del><ins>+ return self.method in ("REQUEST", \
"ADD", "CANCEL", "POLLSTATUS") </ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -393,6 +394,8 @@
</span><span class="cx"> elif self.method == "ADD":
</span><span class="cx"> # TODO: implement ADD
</span><span class="cx"> result = (False, False, False, None)
</span><ins>+ elif self.method == "POLLSTATUS":
+ result = (yield self.doImplicitAttendeePollStatus())
</ins><span class="cx"> else:
</span><span class="cx"> # NB We should never get here as we will have \
rejected unsupported METHODs earlier. </span><span class="cx"> result = \
(True, True, False, None,) </span><span class="lines">@@ -552,7 +555,7 @@
</span><span class="cx"> else:
</span><span class="cx"> # Request needs to be ignored
</span><span class="cx"> log.debug("ImplicitProcessing - \
originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - \
ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid)) \
</span><del>- result = (True, True, False, None,) </del><ins>+ \
result = (True, False, False, None,) </ins><span class="cx">
</span><span class="cx"> returnValue(result)
</span><span class="cx">
</span><span class="lines">@@ -570,7 +573,7 @@
</span><span class="cx"> # If there is no existing copy, then ignore
</span><span class="cx"> if self.recipient_calendar is None:
</span><span class="cx"> log.debug("ImplicitProcessing - originator \
'%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" \
% (self.originator.cuaddr, self.recipient.cuaddr, self.uid)) </span><del>- \
result = (True, True, True, None) </del><ins>+ result = (True, False, \
True, None) </ins><span class="cx"> else:
</span><span class="cx"> # Need to check for auto-respond attendees. \
These need to suppress the inbox message </span><span class="cx"> # if \
the cancel is processed. However, if the principal is a user we always force the \
</span><span class="lines">@@ -631,12 +634,36 @@ </span><span class="cx"> \
result = (True, autoprocessed, store_inbox, changes) </span><span class="cx"> \
else: </span><span class="cx"> log.debug("ImplicitProcessing - \
originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - \
ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid)) \
</span><del>- result = (True, True, False, None) </del><ins>+ \
result = (True, False, False, None) </ins><span class="cx">
</span><span class="cx"> returnValue(result)
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def doImplicitAttendeePollStatus(self):
+ """
+ An iTIP message status update has been sent to an attendee by the organizer. \
We need to update the + attendee state based on the nature of the iTIP \
message. + """
+ # If there is no existing copy, then we must fail
+ if self.new_resource:
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' \
processing METHOD:POLLSTATUS, UID: '%s' - attendee has no copy" % \
(self.originator.cuaddr, self.recipient.cuaddr, self.uid)) + \
returnValue((True, False, False, None,)) +
+ processed_message = iTipProcessing.processPollStatus(self.message, \
self.recipient_calendar) +
+ # Let the store know that no time-range info has changed for a refresh \
(assuming that + # no auto-accept changes were made)
+ processed_message.noInstanceIndexing = True
+
+ # Update the attendee's copy of the event
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' \
processing METHOD:POLLSTATUS, UID: '%s' - updating poll" % \
(self.originator.cuaddr, self.recipient.cuaddr, self.uid)) + yield \
self.writeCalendarResource(None, self.recipient_calendar_resource, processed_message) \
+ + returnValue((True, False, False, None,))
+
+
+ @inlineCallbacks
</ins><span class="cx"> def checkAttendeeAutoReply(self, calendar, automode):
</span><span class="cx"> """
</span><span class="cx"> Check whether a reply to the given iTIP message is \
needed and if so make the </span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingschedulerpy"></a>
<div class="modfile"><h4>Modified: \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py (14352 => \
14353)</h4> <pre class="diff"><span>
<span class="info">--- \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py 2015-01-29 \
20:22:01 UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py 2015-01-29 \
22:44:58 UTC (rev 14353) </span><span class="lines">@@ -515,7 +515,7 @@
</span><span class="cx"> yield \
self.generateRemoteSchedulingResponses(otherserver_recipients, responses, freebusy, \
getattr(self.txn, 'doing_attendee_refresh', False)) </span><span class="cx">
</span><span class="cx"> # To reduce chatter, we suppress certain messages
</span><del>- if not self.suppress_refresh:
</del><ins>+ if not self.suppress_refresh or self.calendar.mainType() == \
"VPOLL": </ins><span class="cx">
</span><span class="cx"> # Now process remote recipients
</span><span class="cx"> if remote_recipients:
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoreschedulingtesttest_itippy"></a>
<div class="modfile"><h4>Modified: \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py (14352 => \
14353)</h4> <pre class="diff"><span>
<span class="info">--- \
CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py 2015-01-29 \
20:22:01 UTC (rev 14352)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py 2015-01-29 \
22:44:58 UTC (rev 14353) </span><span class="lines">@@ -21,7 +21,7 @@
</span><span class="cx"> from twisted.trial import unittest
</span><span class="cx">
</span><span class="cx"> from twistedcaldav.stdconfig import config
</span><del>-from twistedcaldav.ical import Component
</del><ins>+from twistedcaldav.ical import Component, normalize_iCalStr
</ins><span class="cx">
</span><span class="cx"> from txdav.caldav.datastore.scheduling.itip import \
iTipProcessing, iTipGenerator </span><span class="cx">
</span><span class="lines">@@ -1289,6 +1289,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "1.2 Simple Reply - recurring no \
overrides", </span><span class="lines">@@ -1335,6 +1336,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "1.3 Simple Reply - recurring with \
missing master", </span><span class="lines">@@ -1389,6 +1391,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "1.4 Simple Reply - recurring with \
overrides in master but not reply", </span><span class="lines">@@ -1453,6 \
+1456,7 @@ </span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "1.5 Simple Reply - recurring with \
overrides in master invalid in reply", </span><span class="lines">@@ -1517,6 \
+1521,7 @@ </span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "1.6 Simple Reply - recurring with \
overrides in master, invalid ones in reply", </span><span class="lines">@@ \
-1590,6 +1595,7 @@ </span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "2.1 X-CALENDARSERVER-RESET-PARTSTAT \
Reply - non recurring", </span><span class="lines">@@ -1636,6 +1642,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "2.2 X-CALENDARSERVER-RESET-PARTSTAT \
Reply - non recurring", </span><span class="lines">@@ -1682,6 +1689,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "2.3 X-CALENDARSERVER-RESET-PARTSTAT \
Reply - non recurring", </span><span class="lines">@@ -1728,6 +1736,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "2.4 X-CALENDARSERVER-RESET-PARTSTAT \
Reply - non recurring", </span><span class="lines">@@ -1774,6 +1783,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ None,
</ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "3.1 Simple VPOLL Reply - response \
added", </span><span class="lines">@@ -1874,6 +1884,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ ('mailto:user2@example.com', set([("", True, \
False,)])), </ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "3.2 Simple VPOLL Reply - response \
changed", </span><span class="lines">@@ -1978,6 +1989,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ ('mailto:user2@example.com', set([("", True, \
False,)])), </ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "3.3 Simple VPOLL Reply - response \
added and changed", </span><span class="lines">@@ -2090,6 +2102,7 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ ('mailto:user2@example.com', set([("", True, \
False,)])), </ins><span class="cx"> ),
</span><span class="cx"> (
</span><span class="cx"> "3.4 Simple VPOLL Reply - response one \
changed", </span><span class="lines">@@ -2202,21 +2215,475 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> """,
</span><span class="cx"> True,
</span><ins>+ ('mailto:user2@example.com', set([("", True, \
False,)])), </ins><span class="cx"> ),
</span><ins>+ (
+ "3.5 Simple VPOLL Reply - no changes",
+ """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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@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@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;SCHEDULE-STATUS=2.0:mailto:user2@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@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+ True,
+ ('mailto:user2@example.com', set([])),
+ ),
</ins><span class="cx"> )
</span><span class="cx">
</span><del>- for title, calendar_txt, itip_txt, changed_txt, expected in \
data: </del><ins>+ for title, calendar_txt, itip_txt, changed_txt, expected, \
processed in data: </ins><span class="cx"> calendar = \
Component.fromString(calendar_txt) </span><span class="cx"> itip = \
Component.fromString(itip_txt) </span><span class="cx"> if expected:
</span><span class="cx"> changed = Component.fromString(changed_txt)
</span><span class="cx">
</span><del>- result, _ignore = iTipProcessing.processReply(itip, \
calendar) </del><ins>+ result, result_processed = \
iTipProcessing.processReply(itip, calendar) </ins><span class="cx"> \
self.assertEqual(result, expected, msg="Result mismatch: %s" % (title,)) \
</span><span class="cx"> if expected: </span><span class="cx"> \
self.assertEqual(changed, calendar, msg="Calendar mismatch: %s" % (title,)) \
</span><ins>+ if processed is not None: + \
self.assertEqual(result_processed, processed, msg="Process mismatch: %s" % \
(title,)) </ins><span class="cx">
</span><span class="cx">
</span><ins>+ def test_processPollStatus(self):
+ """
+ Test iTIPProcessing.processPollStatus
+ """
+
+ data = (
+ (
+ "3.1 Simple VPOLL - 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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@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:POLLSTATUS
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:0
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+ ),
+ (
+ "3.2 Simple VPOLL - recipient response not 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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2@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:POLLSTATUS
+BEGIN:VPOLL
+UID:8D2B2048-7915-6ECD-A82B-01F4EF8EEBEA
+DTSTAMP:20150113T152404Z
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:50
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:0
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+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@example.com
+POLL-MODE:BASIC
+POLL-PROPERTIES:DTSTART,DTEND
+SUMMARY:New Poll
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user1@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:100
+END:VOTE
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user2@example.com
+END:VVOTER
+BEGIN:VVOTER
+DTSTAMP:20150113T152404Z
+VOTER;RSVP=TRUE:mailto:user3@example.com
+BEGIN:VOTE
+POLL-ITEM-ID:1
+RESPONSE:100
+END:VOTE
+BEGIN:VOTE
+POLL-ITEM-ID:2
+RESPONSE:0
+END:VOTE
+END:VVOTER
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2@example.com
+ORGANIZER;CN=User 01:mailto:user1@example.com
+POLL-ITEM-ID:2
+END:VEVENT
+END:VPOLL
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for title, calendar_txt, itip_txt, changed_txt in data:
+ calendar = Component.fromString(calendar_txt)
+ itip = Component.fromString(itip_txt)
+
+ result = iTipProcessing.processPollStatus(itip, calendar, \
"mailto:user2@example.com") + \
self.assertEqual(normalize_iCalStr(result), normalize_iCalStr(changed_txt), \
msg="Calendar mismatch: %s" % (title,)) +
+
</ins><span class="cx"> def test_update_attendee_partstat(self):
</span><span class="cx">
</span><span class="cx"> data = (
</span></span></pre>
</div>
</div>
</body>
</html>
_______________________________________________
calendarserver-changes mailing list
calendarserver-changes@lists.macosforge.org
https://lists.macosforge.org/mailman/listinfo/calendarserver-changes
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic