[CalendarServer-changes] [14164] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Nov 13 11:56:48 PST 2014


Revision: 14164
          http://trac.calendarserver.org//changeset/14164
Author:   cdaboo at apple.com
Date:     2014-11-13 11:56:48 -0800 (Thu, 13 Nov 2014)
Log Message:
-----------
Client transp fix.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
    CalendarServer/trunk/twistedcaldav/test/test_util.py
    CalendarServer/trunk/twistedcaldav/util.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/sql_external.py
    CalendarServer/trunk/txdav/caldav/icalendarstore.py
    CalendarServer/trunk/txdav/carddav/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql.py

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -868,6 +868,16 @@
     "MaxPrincipalSearchReportResults": 500,
 
     #
+    # Client fixes per user-agent match
+    #
+    "ClientFixes" : {
+        "ForceAttendeeTRANSP" : [
+            "iOS/8\\.0(\\..*)?",
+            "iOS/8\\.1(\\..*)?",
+        ],
+    },
+
+    #
     # Localization
     #
     "Localization" : {
@@ -1488,6 +1498,19 @@
 
 
 
+def _updateClientFixes(configDict, reloading=False):
+    #
+    # Compile ClientFixes expressions for speed
+    #
+    try:
+        configDict.ClientFixesCompiled = {}
+        for key, expressions in configDict.ClientFixes.items():
+            configDict.ClientFixesCompiled[key] = [re.compile("^{}$".format(x)) for x in expressions]
+    except re.error, e:
+        raise ConfigurationError("Invalid regular expression in ClientFixes: %s" % (e,))
+
+
+
 def _updateLogLevels(configDict, reloading=False):
     log.publisher.levels.clearLogLevels()
 
@@ -1678,6 +1701,7 @@
     _postUpdateAugmentService,
     _updateACLs,
     _updateRejectClients,
+    _updateClientFixes,
     _updateLogLevels,
     _updateNotifications,
     _updateICalendar,

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -48,7 +48,7 @@
 from twistedcaldav.sharing import (
     invitationBindStatusToXMLMap, invitationBindModeToXMLMap
 )
-from twistedcaldav.util import bestAcceptType
+from twistedcaldav.util import bestAcceptType, matchClientFixes
 from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
 from txdav.base.propertystore.base import PropertyName
 from txdav.caldav.icalendarstore import (
@@ -59,7 +59,7 @@
     InvalidPerUserDataMerge,
     AttendeeAllowedError, ResourceDeletedError, InvalidAttachmentOperation,
     ShareeAllowedError, DuplicatePrivateCommentsError, InvalidSplit,
-    AttachmentSizeTooLarge, UnknownTimezone)
+    AttachmentSizeTooLarge, UnknownTimezone, SetComponentOptions)
 from txdav.carddav.iaddressbookstore import (
     KindChangeNotAllowedError, GroupWithUnsharedAddressNotAllowedError
 )
@@ -2848,8 +2848,18 @@
                     "Can't parse calendar data: %s" % (str(e),)
                 ))
 
+            # Look for client fixes
+            ua = request.headers.getHeader("User-Agent")
+            client_fix_transp = matchClientFixes(config, ua)
+
+            # Setup options
+            options = {
+                SetComponentOptions.smartMerge: schedule_tag_match,
+                SetComponentOptions.clientFixTRANSP: client_fix_transp,
+            }
+
             try:
-                response = (yield self.storeComponent(component, smart_merge=schedule_tag_match))
+                response = (yield self.storeComponent(component, options=options))
             except ResourceDeletedError:
                 # This is OK - it just means the server deleted the resource during the PUT. We make it look
                 # like the PUT succeeded.

Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -48,7 +48,7 @@
                    _getFakeMemcacheProtocol)
 
         # Need to not do implicit behavior during these tests
-        def _fakeDoImplicitScheduling(self, component, inserting, internal_state):
+        def _fakeDoImplicitScheduling(self, component, inserting, internal_state, options):
             return False, None, False, None
 
         self.patch(CalendarObject, "doImplicitScheduling",

Modified: CalendarServer/trunk/twistedcaldav/test/test_util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_util.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/twistedcaldav/test/test_util.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -16,8 +16,10 @@
 
 from txweb2.http_headers import Headers
 
+from twistedcaldav.config import ConfigDict
+from twistedcaldav.stdconfig import _updateClientFixes
+from twistedcaldav.util import bestAcceptType, userAgentProductTokens, matchClientFixes
 import twistedcaldav.test.util
-from twistedcaldav.util import bestAcceptType
 
 class AcceptType(twistedcaldav.test.util.TestCase):
     """
@@ -142,3 +144,78 @@
                 hdrs.addRawHeader(*hdr)
             check = bestAcceptType(hdrs.getHeader("accept"), allowedTypes)
             self.assertEqual(check, result, msg="Failed %s" % (title,))
+
+
+    def test_userAgentProductTokens(self):
+        """
+        Test that L{userAgentProductTokens} correctly parses a User-Agent header.
+        """
+        for hdr, result in (
+            # Valid syntax
+            ("Client/1.0", ["Client/1.0", ]),
+            ("Client/1.0 FooBar/2", ["Client/1.0", "FooBar/2", ]),
+            ("Client/1.0 (commentary here)", ["Client/1.0", ]),
+            ("Client/1.0 (FooBar/2)", ["Client/1.0", ]),
+            ("Client/1.0 (commentary here) FooBar/2", ["Client/1.0", "FooBar/2", ]),
+            ("Client/1.0 (commentary here) FooBar/2 (more commentary here) ", ["Client/1.0", "FooBar/2", ]),
+
+            # Invalid syntax
+            ("Client/1.0 (commentary here FooBar/2", ["Client/1.0", ]),
+            ("Client/1.0 commentary here) FooBar/2", ["Client/1.0", "commentary", "here)", "FooBar/2", ]),
+        ):
+            self.assertEqual(userAgentProductTokens(hdr), result, msg="Mismatch: {}".format(hdr))
+
+
+    def test_matchClientFixes(self):
+        """
+        Test that L{matchClientFixes} correctly identifies clients with matching fix tokens.
+        """
+        c = ConfigDict()
+        c.ClientFixes = {
+            "fix1": [
+                "Client/1\\.0.*",
+                "Client/1\\.1(\\..*)?",
+                "Client/2",
+            ],
+            "fix2": [
+                "Other/1\\.0.*",
+            ],
+        }
+        _updateClientFixes(c)
+        _updateClientFixes(c)
+
+        # Valid matches
+        for ua in (
+            "Client/1.0 FooBar/2",
+            "Client/1.0.1 FooBar/2",
+            "Client/1.0.1.1 FooBar/2",
+            "Client/1.1 FooBar/2",
+            "Client/1.1.1 FooBar/2",
+            "Client/2 FooBar/2",
+        ):
+            self.assertEqual(
+                matchClientFixes(c, ua),
+                set(("fix1",)),
+                msg="Did not match {}".format(ua),
+            )
+
+        # Valid non-matches
+        for ua in (
+            "Client/1 FooBar/2",
+            "Client/1.10 FooBar/2",
+            "Client/2.0 FooBar/2",
+            "Client/2.0.1 FooBar/2",
+            "Client FooBar/2",
+            "Client/3 FooBar/2",
+            "Client/3.0 FooBar/2",
+            "Client/10 FooBar/2",
+            "Client/10.0 FooBar/2",
+            "Client/10.0.1 FooBar/2",
+            "Client/10.0.1 (Client/1.0) FooBar/2",
+            "Client/10.0.1 (foo Client/1.0 bar) FooBar/2",
+        ):
+            self.assertEqual(
+                matchClientFixes(c, ua),
+                set(),
+                msg="Incorrectly matched {}".format(ua),
+            )

Modified: CalendarServer/trunk/twistedcaldav/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/util.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/twistedcaldav/util.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -18,6 +18,7 @@
 import re
 import sys
 import base64
+import itertools
 
 from subprocess import Popen, PIPE, STDOUT
 from hashlib import md5, sha1
@@ -541,3 +542,57 @@
                 result_qval = qval
 
     return result
+
+
+
+def userAgentProductTokens(user_agent):
+    """
+    Parse an HTTP User-Agent header to extract the product tokens and ignore
+    any parenthesized comment strings in the header.
+
+    @param user_agent: text of User-Agent header value
+    @type user_agent: L{str}
+
+    @return: list of product tokens extracted from the header
+    @rtype: L{list}
+    """
+
+    ua_hdr = user_agent.split()
+    ua_tokens = []
+    comment = False
+    for token in ua_hdr:
+        if comment:
+            if token.endswith(")"):
+                comment = False
+        elif token.startswith("("):
+            if not token.endswith(")"):
+                comment = True
+        else:
+            ua_tokens.append(token)
+
+    return ua_tokens
+
+
+
+def matchClientFixes(config, user_agent):
+    """
+    Given a user-agent string, see if it matches any of the configured client fixes.
+
+    @param config: the L{config} to match against.
+    @type config: L{ConfigDict}
+    @param user_agent: the HTTP User-Agent header value to test.
+    @type user_agent: L{str}
+    """
+
+    if len(config.ClientFixesCompiled) == 0 or not user_agent:
+        return set()
+
+    ua_tokens = userAgentProductTokens(user_agent)
+
+    client_fixes = set()
+    for fix, patterns in config.ClientFixesCompiled.items():
+        for pattern, token in itertools.product(patterns, ua_tokens):
+            if pattern.match(token) is not None:
+                client_fixes.add(fix)
+                break
+    return client_fixes

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -27,10 +27,6 @@
 from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
 from txdav.caldav.datastore.scheduling.itip import iTipGenerator
 
-"""
-Class that handles diff'ing two calendar objects.
-"""
-
 __all__ = [
     "iCalDiff",
 ]
@@ -38,19 +34,32 @@
 log = Logger()
 
 class iCalDiff(object):
+    """
+    This object is used for doing comparisons between two calendar objects to
+    work out what the changes are, in order to determine whether a scheduling
+    operation might be needed. The behavior will varying based on whether the
+    change is being triggered by an Organizer or an Attendee.
+    """
 
-    def __init__(self, oldcalendar, newcalendar, smart_merge):
+    def __init__(self, oldcalendar, newcalendar, smart_merge, forceTRANSP=False):
         """
+        Note that this object will always duplicate the calendar objects when doing
+        comparisons so as not to change the calendar objects passed in.
 
-        @param oldcalendar:
-        @type oldcalendar:
-        @param newcalendar:
-        @type newcalendar:
+        @param oldcalendar: the existing calendar object to compare to
+        @type oldcalendar: L{Component}
+        @param newcalendar: the new calendar object
+        @type newcalendar: L{Component}
+        @param smart_merge: whether or not a "smart" CalDAV merge is done (If-Schedule-Tag-Match)
+        @type smart_merge: L{bool}
+        @param forceTRANSP: whether or not to apply a fix for clients failing to set TRANSP properly
+        @type forceTRANSP: L{bool}
         """
 
         self.oldcalendar = oldcalendar
         self.newcalendar = newcalendar
         self.smart_merge = smart_merge
+        self.forceTRANSP = forceTRANSP
 
 
     def organizerDiff(self):
@@ -501,6 +510,13 @@
 
             replyNeeded = True
 
+            # Apply client fix only if the PARTSTAT was changed
+            if self.forceTRANSP:
+                if clientAttendee.parameterValue("PARTSTAT", "NEEDS-ACTION") in ("ACCEPTED", "TENTATIVE",):
+                    clientComponent.replaceProperty(Property("TRANSP", "OPAQUE"))
+                else:
+                    clientComponent.replaceProperty(Property("TRANSP", "TRANSPARENT"))
+
         if serverAttendee.parameterValue("RSVP", "FALSE") != clientAttendee.parameterValue("RSVP", "FALSE"):
             if clientAttendee.parameterValue("RSVP", "FALSE") == "FALSE":
                 try:

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -37,6 +37,7 @@
 from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord
 from txdav.caldav.datastore.scheduling.work import ScheduleReplyWork, \
     ScheduleReplyCancelWork, ScheduleOrganizerWork, ScheduleOrganizerSendWork
+from txdav.caldav.icalendarstore import SetComponentOptions
 
 import collections
 
@@ -65,10 +66,11 @@
     STATUS_ORPHANED_CANCELLED_EVENT = 1
     STATUS_ORPHANED_EVENT = 2
 
-    def __init__(self, logItems=None):
+    def __init__(self, logItems=None, options=None):
 
         self.return_status = ImplicitScheduler.STATUS_OK
         self.logItems = logItems
+        self.options = options
         self.allowed_to_schedule = True
         self.suppress_refresh = False
 
@@ -1683,7 +1685,11 @@
                     (caldav_namespace, "valid-attendee-change"),
                     "Cannot use an event when not listed as an attendee in the organizer's copy",
                 ))
-        differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
+
+        # Check for a required client fix
+        forceTRANSP = SetComponentOptions.value(self.options, SetComponentOptions.clientFixTRANSP)
+
+        differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge, forceTRANSP=forceTRANSP)
         return differ.attendeeMerge(self.attendee)
 
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -5248,6 +5248,391 @@
             self.assertEqual(got_rescheduled, rescheduled, msg="%s expected rescheduled: '%s', got: '%s'" % (description, rescheduled, got_rescheduled,))
 
 
+    def test_attendee_merge_forceTRANSP(self):
+
+        data = (
+            (
+                "#1.1 Simple component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.2 Simple component, change to ACCEPTED",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;X-CALENDARSERVER-DTSTAMP=XXXXXXXXTXXXXXXZ:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.3 Simple component, change to DECLINED",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=DECLINED:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED;X-CALENDARSERVER-DTSTAMP=XXXXXXXXTXXXXXXZ:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.4 Simple component, change to TENTATIVE",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=TENTATIVE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE;X-CALENDARSERVER-DTSTAMP=XXXXXXXXTXXXXXXZ:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.5 Simple component, change to ACCEPTED with TRANSP",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=TENTATIVE:mailto:user2 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE;X-CALENDARSERVER-DTSTAMP=XXXXXXXXTXXXXXXZ:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.6 Simple component, remain as ACCEPTED with TRANSP",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.7 Simple component, remain as ACCEPTED change TRANSP",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.8 Simple component, ACCEPTED->DECLINED with TRANSP",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+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;PARTSTAT=DECLINED:mailto:user2 at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED;X-CALENDARSERVER-DTSTAMP=XXXXXXXXTXXXXXXZ:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+        )
+
+        for description, calendar1, calendar2, attendee, result in data:
+            differ = iCalDiff(
+                Component.fromString(calendar1),
+                Component.fromString(calendar2),
+                False,
+                forceTRANSP=True,
+            )
+            diffResult = differ.attendeeMerge(attendee)
+            diffResult = (
+                diffResult[0],
+                diffResult[1],
+                tuple(sorted(diffResult[2])),
+                re.sub(
+                    "X-CALENDARSERVER-DTSTAMP=[^Z]+",
+                    "X-CALENDARSERVER-DTSTAMP=XXXXXXXXTXXXXXX",
+                    str(diffResult[3]).replace("\r", "").replace("\n ", "")
+                ) if diffResult[3] else None,
+            )
+            self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
+
     def test_organizer_smart_merge(self):
 
         data1 = (

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -78,7 +78,7 @@
     InvalidDefaultCalendar, \
     InvalidAttachmentOperation, DuplicatePrivateCommentsError, \
     TimeRangeUpperLimit, TimeRangeLowerLimit, InvalidSplit, \
-    AttachmentSizeTooLarge, UnknownTimezone
+    AttachmentSizeTooLarge, UnknownTimezone, SetComponentOptions
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
     CommonObjectResource, ECALENDARTYPE, SharingInvitation
@@ -3041,7 +3041,7 @@
 
 
     @inlineCallbacks
-    def doImplicitScheduling(self, component, inserting, internal_state, split_details=None):
+    def doImplicitScheduling(self, component, inserting, internal_state, options, split_details=None):
 
         new_component = None
         did_implicit_action = False
@@ -3063,7 +3063,7 @@
                 user_uuid = self._parentCollection.viewerHome().uid()
                 component = PerUserDataFilter(user_uuid).filter(component.duplicate())
 
-            scheduler = ImplicitScheduler(logItems=self._txn.logItems)
+            scheduler = ImplicitScheduler(logItems=self._txn.logItems, options=options)
 
             # PUT
             do_implicit_action, is_scheduling_resource = (yield scheduler.testImplicitSchedulingPUT(
@@ -3200,7 +3200,7 @@
 
 
     @inlineCallbacks
-    def setComponent(self, component, inserting=False, smart_merge=False):
+    def setComponent(self, component, inserting=False, options=None):
         """
         Public api for storing a component. This will do full data validation checks on the specified component.
         Scheduling will be done automatically.
@@ -3213,7 +3213,7 @@
             except InvalidICalendarDataError as e:
                 raise InvalidComponentForStoreError(str(e))
         try:
-            result = yield self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL, smart_merge)
+            result = yield self._setComponentInternal(component, inserting, ComponentUpdateState.NORMAL, options)
         except Exception:
             ex = Failure()
 
@@ -3228,7 +3228,7 @@
 
 
     @inlineCallbacks
-    def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, smart_merge=False, split_details=None):
+    def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, options=None, split_details=None):
         """
         Setting the component internally to the store itself. This will bypass a whole bunch of data consistency checks
         on the assumption that those have been done prior to the component data being provided, provided the flag is set.
@@ -3236,7 +3236,11 @@
         """
 
         self._componentChanged = False
-        self.schedule_tag_match = not self.calendar().isInbox() and internal_state == ComponentUpdateState.NORMAL and smart_merge
+        self.schedule_tag_match = (
+            not self.calendar().isInbox() and
+            internal_state == ComponentUpdateState.NORMAL and
+            SetComponentOptions.value(options, SetComponentOptions.smartMerge)
+        )
         schedule_state = None
 
         if internal_state in (ComponentUpdateState.SPLIT_OWNER, ComponentUpdateState.SPLIT_ATTENDEE,):
@@ -3255,7 +3259,7 @@
 
             # Do scheduling only for owner split
             if internal_state == ComponentUpdateState.SPLIT_OWNER:
-                yield self.doImplicitScheduling(component, inserting, internal_state, split_details)
+                yield self.doImplicitScheduling(component, inserting, internal_state, options, split_details)
 
             self.isScheduleObject = True
             self.processScheduleTags(component, inserting, internal_state)
@@ -3291,7 +3295,7 @@
                 yield self.decorateHostedStatus(component)
 
             # Do scheduling
-            implicit_result = (yield self.doImplicitScheduling(component, inserting, internal_state))
+            implicit_result = (yield self.doImplicitScheduling(component, inserting, internal_state, options))
             if isinstance(implicit_result, int):
                 if implicit_result == ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT:
                     raise ResourceDeletedError("Resource created but immediately deleted by the server.")

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql_external.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql_external.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql_external.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -171,7 +171,7 @@
         raise AssertionError("CalendarObjectExternal: not supported")
 
 
-    def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, smart_merge=False, split_details=None):
+    def _setComponentInternal(self, component, inserting=False, internal_state=ComponentUpdateState.NORMAL, options=None, split_details=None):
         raise AssertionError("CalendarObjectExternal: not supported")
 
 

Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -950,3 +950,30 @@
     NORMAL.description = "normal"
     NORMAL_NO_IMPLICIT.description = "normal-no-implicit"
     INTERNAL.description = "internal"
+
+
+
+class SetComponentOptions(Names):
+    """
+    Constants for keys used in the L{ICalendarObject.setComponent} method's
+    C{options} dict. The definitions below define the constant key name and
+    the type used for the dict entry's value.
+
+    @cvar smartMerge: Apply CalDAV smart merge to data (If-Schedule-Tag-Match)
+        Value: L{bool}
+
+    @cvar clientFixTRANSP: Apply fix for clients not setting TRANSP.
+        Value: L{bool}
+    """
+
+    smartMerge = NamedConstant()
+    smartMerge.description = u"Smart Merge: CalDAV If-Schedule-Tag-Match behavior"
+    smartMerge.defaultValue = False
+
+    clientFixTRANSP = NamedConstant()
+    clientFixTRANSP.description = u"Fix for clients not setting TRANSP"
+    clientFixTRANSP.defaultValue = False
+
+    @staticmethod
+    def value(options, key):
+        return options.get(key, key.defaultValue) if options is not None else key.defaultValue

Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -2041,7 +2041,7 @@
 
 
     @inlineCallbacks
-    def remove(self):
+    def remove(self, options=None):
 
         if self.owned():
             yield self.unshare() # storebridge should already have done this
@@ -2412,7 +2412,7 @@
 
 
     @inlineCallbacks
-    def setComponent(self, component, inserting=False):
+    def setComponent(self, component, inserting=False, options=None):
 
         if isinstance(component, str) or isinstance(component, unicode):
             component = self._componentClass.fromString(component)

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2014-11-13 19:56:11 UTC (rev 14163)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2014-11-13 19:56:48 UTC (rev 14164)
@@ -7238,7 +7238,7 @@
         self._locked = True
 
 
-    def setComponent(self, component, inserting=False):
+    def setComponent(self, component, inserting=False, options=None):
         raise NotImplementedError
 
 
@@ -7285,7 +7285,7 @@
 
 
     @inlineCallbacks
-    def remove(self):
+    def remove(self, options=None):
         yield self._deleteQuery.on(self._txn, NoSuchObjectResourceError,
                                    resourceID=self._resourceID)
         yield self.properties()._removeResource()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20141113/b33946a3/attachment-0001.html>


More information about the calendarserver-changes mailing list