[CalendarServer-changes] [2990] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Fri Sep 12 09:30:41 PDT 2008


Revision: 2990
          http://trac.macosforge.org/projects/calendarserver/changeset/2990
Author:   cdaboo at apple.com
Date:     2008-09-12 09:30:40 -0700 (Fri, 12 Sep 2008)
Log Message:
-----------
Merged implicitauto brnach to trunk. This fixes auto-accept and proxy implicit scheduling behavior.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/itip.py
    CalendarServer/trunk/twistedcaldav/memcacher.py
    CalendarServer/trunk/twistedcaldav/method/put_common.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
    CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/memcachelock.py
    CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -1291,7 +1291,18 @@
             if component.name() == "VTIMEZONE":
                 continue
             component.addProperty(property)
+
+    def replacePropertyInAllComponents(self, property):
+        """
+        Replace a property in all components.
+        @param property: the L{Property} to replace in this component.
+        """
         
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            component.replaceProperty(property)
+        
     def attendeesView(self, attendees):
         """
         Filter out any components that all attendees are not present in. Use EXDATEs
@@ -1338,11 +1349,10 @@
 
         assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
 
-        if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-                [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value() != attendee]
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value() != attendee]
             
     def removeAlarms(self):
         """
@@ -1366,14 +1376,13 @@
 
         assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
 
-        if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-                if keep:
-                    [component.removeProperty(p) for p in tuple(component.properties()) if p.name() not in keep]
-                if remove:
-                    [component.removeProperty(p) for p in tuple(component.properties()) if p.name() in remove]
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            if keep:
+                [component.removeProperty(p) for p in tuple(component.properties()) if p.name() not in keep]
+            if remove:
+                [component.removeProperty(p) for p in tuple(component.properties()) if p.name() in remove]
                 
     def removeXProperties(self, keep_properties):
         """
@@ -1382,15 +1391,14 @@
 
         assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
 
-        if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-                [
-                    component.removeProperty(p)
-                    for p in tuple(component.properties())
-                    if p.name().startswith("X-") and p.name() not in keep_properties
-                ]
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            [
+                component.removeProperty(p)
+                for p in tuple(component.properties())
+                if p.name().startswith("X-") and p.name() not in keep_properties
+            ]
             
     def removePropertyParameters(self, property, params):
         """
@@ -1399,18 +1407,34 @@
 
         assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
 
-        if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-                props = component.properties(property)
-                for prop in props:
-                    for param in params:
-                        try:
-                            del prop.params()[param]
-                        except KeyError:
-                            pass
-            
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            props = component.properties(property)
+            for prop in props:
+                for param in params:
+                    try:
+                        del prop.params()[param]
+                    except KeyError:
+                        pass
+
+    def normalizePropertyValueLists(self, propname):
+        """
+        Convert properties that have a list of values into single properties, to make it easier
+        to do comparisons between two ical objects.
+        """
+        
+        assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            for prop in tuple(component.properties(propname)):
+                if type(prop.value()) is list and len(prop.value()) > 1:
+                    component.removeProperty(prop)
+                    for value in prop.value():
+                        component.addProperty(Property(propname, [value,]))
+        
 ##
 # Dates and date-times
 ##

Modified: CalendarServer/trunk/twistedcaldav/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/itip.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/itip.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -35,7 +35,8 @@
 import time
 
 from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred,\
+    succeed
 from twisted.web2.dav import davxml
 from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
 from twisted.web2.dav.util import joinURL
@@ -45,7 +46,8 @@
 from twistedcaldav import caldavxml
 from twistedcaldav.accounting import accountingEnabled, emitAccounting
 from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID
+from twistedcaldav.ical import Property
+from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.method import report_common
 from twistedcaldav.resource import isCalendarCollectionResource
 
@@ -63,6 +65,7 @@
 
 class iTipProcessor(object):
     
+    @inlineCallbacks
     def handleRequest(self, request, principal, inbox, calendar, child):
         """
         Handle an iTIP response automatically.
@@ -93,7 +96,19 @@
         else:
             self.childname = ""
  
-        return f()
+        # Get a lock on the inbox first
+        _lock = MemcacheLock("iTIPAutoProcess", inbox.fp.path, timeout=60.0, retry_interval=1.0, expire_time=300)
+        
+        try:
+            yield _lock.acquire()
+            yield f()
+            yield _lock.release()
+        except MemcacheLockTimeoutError:
+            raise
+        except Exception, e:
+            log.error(e)
+            yield _lock.clean()
+            raise
 
     @inlineCallbacks
     def processRequest(self):
@@ -108,10 +123,8 @@
                   2. See if this updates existing ones in free-busy-set calendars.
                   3. Remove existing ones in those calendars.
                   4. See if this fits into a free slot:
-                      1. If not, send REPLY with failure status
-                      2. If so
-                          1. send REPLY with success
-                          2. add to f-b-s calendar
+                      1. If not, add to f-b-s calendar DECLINED
+                      2. If so, add to f-b-s calendar ACCEPTED
               2. If not,
                   1. remove the one we got - its 'stale'
               3. Delete the request from the Inbox.
@@ -153,30 +166,24 @@
                 
             if check_reply:
                 # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-                doreply, replycal, accepted = yield self.checkForReply()
+                valid, accepted = yield self.checkForReply()
                 
-                try:
-                    if accepted:
+                if valid:
+                    try:
                         if calmatch:
+                            log.info("Replaced calendar component %s with new iTIP message in %s (%s)." % (calmatch, calURL, accepted,))
                             yield self.writeResource(calURL, updatecal, calmatch, self.calendar)
-                            log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
                         else:
+                            log.info("Added new calendar component in %s (%s)." % (calURL, accepted,))
                             yield self.writeResource(calURL, updatecal, None, self.calendar)
-                            log.info("Added new calendar component in %s." % (calURL,))
-                    else:
-                        if calmatch:
-                            yield self.deleteResource(updatecal, calmatch)
-                            log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
-                            
-                    # Send a reply if needed. 
-                    if doreply:
-                        log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                        yield self.writeReply(replycal)
-                    processed = "processed"
-                except:
-                    # FIXME: bare except
-                    log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                    raise iTipException()
+    
+                        processed = "processed"
+                    except:
+                        # FIXME: bare except
+                        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+                        raise iTipException()
+                else:
+                    processed = "ignored"
                 
         else:
             # So we have a partial update. That means we have to do partial updates to instances in
@@ -215,29 +222,26 @@
     
             if check_reply:
                 # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-                doreply, replycal, accepted = yield self.checkForReply()
+                valid, accepted = yield self.checkForReply()
                 
-                try:
-                    if calmatch:
-                        # Merge the new instances with the old ones
-                        self.mergeComponents(self.calendar, cal)
-                        yield self.writeResource(calURL, updatecal, calmatch, cal)
-                        log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
-                    else:
-                        if accepted:
+                if valid:
+                    try:
+                        if calmatch:
+                            # Merge the new instances with the old ones
+                            self.mergeComponents(self.calendar, cal)
+                            log.info("Merged calendar component %s with new iTIP message in %s (%s)." % (calmatch, calURL, accepted,))
+                            yield self.writeResource(calURL, updatecal, calmatch, cal)
+                        else:
+                            log.info("Added new calendar component in %s (%s)." % (calURL, accepted,))
                             yield self.writeResource(calURL, updatecal, None, self.calendar)
-                            log.info("Added new calendar component in %s." % (calURL,))
                             
-                    # Do reply if needed. 
-                    if doreply:
-                        log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                        yield self.writeReply(replycal)
-                        
-                    processed = "processed"
-                except:
-                    # FIXME: bare except
-                    log.err("Error while auto-processing iTIP: %s" % (Failure(),))
-                    raise iTipException()
+                        processed = "processed"
+                    except:
+                        # FIXME: bare except
+                        log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+                        raise iTipException()
+                else:
+                    processed = "ignored"
     
         # Remove the now processed incoming request.
         if self.inbox:
@@ -315,8 +319,8 @@
                 if self.compareSyncInfo(info, newinfo) < 0:
                     # Delete existing resource which has been cancelled
                     try:
-                        yield self.deleteResource(updatecal, calmatch,)
                         log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
+                        yield self.deleteResource(updatecal, calmatch,)
                     except:
                         # FIXME: bare except
                         log.err("Error while auto-processing iTIP: %s" % (Failure(),))
@@ -371,12 +375,12 @@
                     # in which case the calendar object is empty (except for VTIMEZONEs).
                     if existing_calendar.mainType() is None:
                         # Delete the now empty calendar object
-                        yield self.deleteResource(updatecal, calmatch)
                         log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+                        yield self.deleteResource(updatecal, calmatch)
                     else:
                         # Update the existing calendar object
-                        yield self.writeResource(calURL, updatecal, calmatch, existing_calendar)
                         log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
+                        yield self.writeResource(calURL, updatecal, calmatch, existing_calendar)
                     processed = "processed"
                 else:
                     processed = "older"
@@ -405,7 +409,7 @@
         BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
         At the moment we will treat a failure on one instance as a DECLINE of the entire set.
 
-        @return: C{True} if a reply is needed, C{False} otherwise.
+        @return: a C{tuple} of C{bool} indicating whether a valid iTIP was received, and C{str} new partstat.
         """
         
         # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
@@ -454,58 +458,21 @@
         cuas = self.principal.calendarUserAddresses()
         attendeeProps = self.calendar.getAttendeeProperties(cuas)
         if not attendeeProps:
-            returnValue((False, None, accepted))
+            returnValue((False, "",))
     
-        # Look for specific parameters
-        rsvp = True
-        for attendeeProp in attendeeProps:
-            if "RSVP" in attendeeProp.params():
-                if attendeeProp.params()["RSVP"][0] == "FALSE":
-                    rsvp = False
-        
-                # Now modify the original component
-                del attendeeProp.params()["RSVP"]
-    
         if accepted:
             partstat = "ACCEPTED"
         else:
             partstat = "DECLINED"
+            
+            # Make sure declined events are TRANSPARENT on the calendar
+            self.calendar.replacePropertyInAllComponents(Property("TRANSP", "TRANSPARENT"))
+
         for attendeeProp in attendeeProps:
-            if "PARTSTAT" in attendeeProp.params():
-                attendeeProp.params()["PARTSTAT"][0] = partstat
-            else:
-                attendeeProp.params()["PARTSTAT"] = [partstat]
+            attendeeProp.params()["PARTSTAT"] = [partstat]
         
-        # Now create a new calendar object for the reply
-        
-        # First get useful props from the original
-        replycal = self.calendar.duplicate()
-        
-        # Change METHOD
-        replycal.getProperty("METHOD").setValue("REPLY")
-        
-        # Change PRODID to this server
-        replycal.getProperty("PRODID").setValue(iCalendarProductID)
-        
-        # Add REQUEST-STATUS
-        for component in replycal.subcomponents():
-            if accepted:
-                component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
-            else:
-                component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
+        returnValue((True, partstat,))
     
-        # Remove all attendees other than ourselves
-        for component in replycal.subcomponents():
-            if component.name() == "VTIMEZONE":
-                continue
-            attendeeProp = component.getAttendeeProperty(cuas)
-            attendees = tuple(component.properties("ATTENDEE"))
-            for attendee in attendees:
-                if attendeeProp is None or (attendee.value() != attendeeProp.value()):
-                    component.removeProperty(attendee)
-    
-        returnValue((rsvp, replycal, accepted))
-    
     @inlineCallbacks
     def writeReply(self, replycal):
         """
@@ -583,7 +550,8 @@
                      destinationparent = collection,
                      destinationcal = True,
                      calendar = calendar,
-                     isiTIP = itipper
+                     isiTIP = itipper,
+                     internal_request=True,
                  ).run()
         
         returnValue(newchild)
@@ -614,9 +582,9 @@
     def deleteInboxResource(self, processed_state):
         # Remove the now processed incoming request.
         try:
-            yield self.deleteResource(self.inbox, self.childname)
-            log.info("Deleted new iTIP message %s in Inbox because it has been %s." %
+            log.info("Deleting new iTIP message %s in Inbox because it has been %s." %
                 (self.childname, processed_state,))
+            yield self.deleteResource(self.inbox, self.childname)
         except:
             # FIXME: bare except
             log.err("Error while auto-processing iTIP: %s" % (Failure(),))
@@ -632,6 +600,12 @@
         """
         
         delchild = collection.getChild(name)
+        
+        # Sometimes the resource might already be gone...
+        if delchild is None:
+            log.warn("Nothing to delete: %s in %s is missing." % (name, collection))
+            return succeed(None)
+
         index = collection.index()
         index.deleteResource(delchild.fp.basename())
         

Copied: CalendarServer/trunk/twistedcaldav/memcachelock.py (from rev 2989, CalendarServer/branches/users/cdaboo/implicitauto-2947/twistedcaldav/memcachelock.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcachelock.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/memcachelock.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -0,0 +1,117 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.memcacher import Memcacher
+from twisted.internet.defer import inlineCallbacks, Deferred, returnValue,\
+    succeed
+from twisted.internet import reactor
+import time
+
+class MemcacheLock(Memcacher):
+
+    def __init__(self, namespace, locktoken, timeout=5.0, retry_interval=0.1, expire_time=0):
+        """
+        
+        @param namespace: a unique namespace for this lock's tokens
+        @type namespace: C{str}
+        @param locktoken: the name of the locktoken
+        @type locktoken: C{str}
+        @param timeout: the maximum time in seconds that the lock should block
+        @type timeout: C{float}
+        @param retry_interval: the interval to retry acquiring the lock
+        @type retry_interval: C{float}
+        @param expiryTime: the time in seconds for the lock to expire. Zero: no expiration.
+        @type expiryTime: C{float}
+        """
+
+        super(MemcacheLock, self).__init__(namespace)
+        self._locktoken = locktoken
+        self._timeout = timeout
+        self._retry_interval = retry_interval
+        self._expire_time = expire_time
+        self._hasLock = False
+
+    def _getMemcacheProtocol(self):
+        
+        result = super(MemcacheLock, self)._getMemcacheProtocol()
+
+        if isinstance(result, Memcacher.nullCacher):
+            raise AssertionError("No implementation of shared locking without memcached")
+        
+        return result
+
+    @inlineCallbacks
+    def acquire(self):
+        
+        assert not self._hasLock, "Lock already acquired."
+    
+        start_time = time.time()
+        waiting = False
+        while time.time() < start_time + self._timeout:
+            
+            result = (yield self.add(self._locktoken, "1", self._expire_time))
+            if result:
+                self._hasLock = True
+                if waiting:
+                    self.log_debug("Got lock after waiting on %s" % (self._locktoken,))
+                break
+            
+            waiting = True
+            self.log_debug("Waiting for lock on %s" % (self._locktoken,))
+            pause = Deferred()
+            def _timedDeferred():
+                pause.callback(True)
+            reactor.callLater(self._retry_interval, _timedDeferred)
+            yield pause
+        else:
+            self.log_debug("Timed out lock after waiting on %s" % (self._locktoken,))
+            raise MemcacheLockTimeoutError()
+        
+        returnValue(True)
+
+    def release(self):
+        
+        assert self._hasLock, "Lock not acquired."
+    
+        def _done(result):
+            self._hasLock = False
+            return result
+
+        d = self.delete(self._locktoken)
+        d.addCallback(_done)
+        return d
+
+    def clean(self):
+        
+        if self._hasLock:
+            return self.delete(self._locktoken)
+        else:
+            return succeed(True)
+
+    def locked(self):
+        """
+        Test if the lock is currently being held.
+        """
+        
+        def _gotit(value):
+            return value is not None
+
+        d = self.get(self._locktoken)
+        d.addCallback(_gotit)
+        return d
+
+class MemcacheLockTimeoutError(Exception):
+    pass

Modified: CalendarServer/trunk/twistedcaldav/memcacher.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcacher.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/memcacher.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -34,7 +34,15 @@
         def __init__(self):
             self._cache = {}
 
-        def set(self, key, value):
+
+        def add(self, key, value, expireTime=0):
+            if key not in self._cache:
+                self._cache[key] = value
+                return succeed(True)
+            else:
+                return succeed(False)
+
+        def set(self, key, value, expireTime=0):
             self._cache[key] = value
             return succeed(True)
 
@@ -59,9 +67,12 @@
         does not actually cache anything.
         """
 
-        def set(self, key, value):
+        def add(self, key, value, expireTime=0):
             return succeed(True)
 
+        def set(self, key, value, expireTime=0):
+            return succeed(True)
+
         def get(self, key):
             return succeed((0, None,))
 
@@ -109,12 +120,21 @@
             return self._memcacheProtocol
 
 
-    def set(self, key, value):
+    def add(self, key, value, expire_time=0):
         my_value = value
         if self._pickle:
             my_value = cPickle.dumps(value)
+        self.log_debug("Adding Cache Token for %r" % (key,))
+        return self._getMemcacheProtocol().add(
+            '%s:%s' % (self._namespace, key), my_value, expireTime=expire_time)
+
+    def set(self, key, value, expire_time=0):
+        my_value = value
+        if self._pickle:
+            my_value = cPickle.dumps(value)
+        self.log_debug("Setting Cache Token for %r" % (key,))
         return self._getMemcacheProtocol().set(
-            '%s:%s' % (self._namespace, key), my_value)
+            '%s:%s' % (self._namespace, key), my_value, expireTime=expire_time)
 
     def get(self, key):
         def _gotit(result):

Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -183,6 +183,7 @@
         calendar=None,
         isiTIP=False,
         allowImplicitSchedule=True,
+        internal_request=False,
     ):
         """
         Function that does common PUT/COPY/MOVE behavior.
@@ -201,6 +202,7 @@
         @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
         @param isiTIP:                True if relaxed calendar data validation is to be done, False otherwise.
         @param allowImplicitSchedule: True if implicit scheduling should be attempted, False otherwise.
+        @param internal_request:   True if this request originates internally and needs to bypass scheduling authorization checks.
         """
         
         # Check that all arguments are valid
@@ -239,6 +241,7 @@
         self.deletesource = deletesource
         self.isiTIP = isiTIP
         self.allowImplicitSchedule = allowImplicitSchedule
+        self.internal_request = internal_request
         
         self.rollback = None
         self.access = None
@@ -690,7 +693,7 @@
             # Do scheduling
             if not self.isiTIP and self.allowImplicitSchedule:
                 scheduler = ImplicitScheduler()
-                new_calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False))
+                new_calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False, internal_request=self.internal_request))
                 if new_calendar:
                     self.calendar = new_calendar
                     self.calendardata = str(self.calendar)

Modified: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -135,7 +135,7 @@
         for principal, inbox, child in autoresponses:
             # Add delayed reactor task to handle iTIP responses
             itip = iTipProcessor()
-            reactor.callLater(0.0, itip.handleRequest, *(self.scheduler.request, principal, inbox, self.scheduler.calendar.duplicate(), child))
+            reactor.callLater(2.0, itip.handleRequest, *(self.scheduler.request, principal, inbox, self.scheduler.calendar.duplicate(), child))
 
     @inlineCallbacks
     def generateResponse(self, recipient, responses, autoresponses):
@@ -147,20 +147,24 @@
         childURL = joinURL(recipient.inboxURL, name)
         child = (yield self.scheduler.request.locateResource(childURL))
 
-        # Do implicit scheduling message processing
-        try:
-            processor = ImplicitProcessor()
-            processed, autoprocessed = (yield processor.doImplicitProcessing(
-                self.scheduler.request,
-                self.scheduler.calendar,
-                self.scheduler.originator,
-                recipient
-            ))
-        except ImplicitProcessorException, e:
-            log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
-            err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=e.msg)
-            returnValue(False)
+        # Do implicit scheduling message processing - but not for auto-accept principals.
+        # Auto-accepts will be processed internally by the server a little later on.
+        if not recipient.principal.autoSchedule():
+            try:
+                processor = ImplicitProcessor()
+                processed, autoprocessed = (yield processor.doImplicitProcessing(
+                    self.scheduler.request,
+                    self.scheduler.calendar,
+                    self.scheduler.originator,
+                    recipient
+                ))
+            except ImplicitProcessorException, e:
+                log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+                err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=e.msg)
+                returnValue(False)
+        else:
+            processed = autoprocessed = False
 
         if autoprocessed:
             # No need to write the inbox item as it has already been auto-processed
@@ -198,7 +202,7 @@
                 child.writeDeadProperty(caldavxml.ScheduleState(caldavxml.ScheduleProcessed() if processed else caldavxml.ScheduleUnprocessed()))
             
                 # Look for auto-schedule option
-                if not processed and recipient.principal.autoSchedule():
+                if recipient.principal.autoSchedule():
                     autoresponses.append((recipient.principal, recipient.inbox, child))
                     
                 returnValue(True)

Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -85,11 +85,13 @@
 
         # Do straight comparison without alarms
         self.calendar1 = self.calendar1.duplicate()
+        self.calendar1.attendeesView((attendee,))
+        self.calendar1.normalizePropertyValueLists("EXDATE")
         self.calendar1.removeXProperties(("X-CALENDARSERVER-PRIVATE-COMMENT",))
-        self.calendar1.attendeesView((attendee,))
         iTipGenerator.prepareSchedulingMessage(self.calendar1)
 
         self.calendar2 = self.calendar2.duplicate()
+        self.calendar2.normalizePropertyValueLists("EXDATE")
         self.calendar2.removeXProperties(("X-CALENDARSERVER-PRIVATE-COMMENT",))
         iTipGenerator.prepareSchedulingMessage(self.calendar2)
 
@@ -100,10 +102,12 @@
         
         # Make sure the same VCALENDAR properties match
         if not self._checkVCALENDARProperties():
+            log.debug("attendeeMerge: VCALENDAR properties do not match")
             return False, False
         
         # Make sure the same VTIMEZONE components appear
         if not self._compareVTIMEZONEs():
+            log.debug("attendeeMerge: VTIMEZONEs do not match")
             return False, False
         
         # Compare each component instance from the new calendar with each derived
@@ -186,6 +190,7 @@
         for key in set2 - set1:
             component1 = self.calendar1.deriveInstance(key[2])
             if component1 is None:
+                log.debug("_compareComponents: Could not derive instance: %s" % (key[2],))
                 return False, False
             component2 = map2[key]
             

Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -25,6 +25,7 @@
 from twistedcaldav.method import report_common
 from twistedcaldav.scheduling.icaldiff import iCalDiff
 from twistedcaldav import caldavxml
+from twisted.web2.dav import davxml
 
 __all__ = [
     "ImplicitScheduler",
@@ -47,7 +48,7 @@
         pass
 
     @inlineCallbacks
-    def doImplicitScheduling(self, request, resource, calendar, deleting):
+    def doImplicitScheduling(self, request, resource, calendar, deleting, internal_request=False):
         """
         Do implicit scheduling operation based on the calendar data that is being PUT
 
@@ -69,7 +70,7 @@
         self.calendar = calendar
         self.calendar_owner = (yield self.resource.owner(self.request))
         self.deleting = deleting
-        self.internal_request = False
+        self.internal_request = internal_request
         self.except_attendees = ()
 
         # When deleting we MUST have the calendar as the actual resource
@@ -77,7 +78,7 @@
         assert deleting and calendar or not deleting
 
         # Get some useful information from the calendar
-        self.extractCalendarData()
+        yield self.extractCalendarData()
 
         # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
         if self.isOrganizerScheduling():
@@ -89,6 +90,7 @@
 
         returnValue(self.calendar)
 
+    @inlineCallbacks
     def refreshAllAttendeesExceptSome(self, request, resource, calendar, attendees):
         """
         
@@ -109,13 +111,33 @@
         self.except_attendees = attendees
         
         # Get some useful information from the calendar
-        self.extractCalendarData()
+        yield self.extractCalendarData()
         self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
         
-        return self.processRequests()
+        result = (yield self.processRequests())
 
+        returnValue(result)
+
+    @inlineCallbacks
     def extractCalendarData(self):
         
+        # Get the originator who is the authenticated user
+        self.originatorPrincipal = None
+        self.originator = ""
+        authz_principal = self.resource.currentPrincipal(self.request).children[0]
+        if isinstance(authz_principal, davxml.HRef):
+            originatorPrincipalURL = str(authz_principal)
+            if originatorPrincipalURL:
+                self.originatorPrincipal = (yield self.request.locateResource(originatorPrincipalURL))
+                if self.originatorPrincipal:
+                    # Pick the first mailto cu address or the first other type
+                    for item in self.originatorPrincipal.calendarUserAddresses():
+                        if not self.originator:
+                            self.originator = item
+                        if item.startswith("mailto:"):
+                            self.originator = item
+                            break
+
         # Get the ORGANIZER and verify it is the same for all components
         organizers = self.calendar.getOrganizersByInstance()
         self.organizer = None
@@ -173,8 +195,30 @@
         
         return False
 
+    @inlineCallbacks
+    def doAccessControl(self, principal, is_organizer):
+        """
+        Check that the currently authorized user has the appropriate scheduling privilege
+        on the principal's Outbox.
+
+        @param principal:
+        @type principal:
+        @param is_organizer:
+        @type is_organizer:
+        """
+        
+        # Find outbox
+        outboxURL = principal.scheduleOutboxURL()
+        outbox = (yield self.request.locateResource(outboxURL))
+        yield outbox.authorize(self.request, (caldavxml.Schedule(),))
+
+    @inlineCallbacks
     def doImplicitOrganizer(self):
         
+        # Do access control
+        if not self.internal_request:
+            yield self.doAccessControl(self.organizerPrincipal, True)
+
         # Check for a delete
         if self.deleting:
 
@@ -194,7 +238,7 @@
             if self.isChangeInsignificant():
                 # Nothing to do
                 log.debug("Implicit - organizer '%s' is updating UID: '%s' but change is not significant" % (self.organizer, self.uid))
-                return
+                returnValue(None)
             
             log.debug("Implicit - organizer '%s' is updating UID: '%s'" % (self.organizer, self.uid))
 
@@ -205,7 +249,7 @@
             self.oldcalendar = None
             self.cancelledAttendees = ()   
             
-        return self.scheduleWithAttendees()
+        yield self.scheduleWithAttendees()
 
     def isChangeInsignificant(self):
         
@@ -324,7 +368,7 @@
     
             # Do the PUT processing
             log.info("Implicit CANCEL - organizer: '%s' to attendee: '%s', UID: '%s', RIDs: '%s'" % (self.organizer, attendee, self.uid, rids))
-            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg, self.internal_request))
+            response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, self.internal_request))
             self.handleSchedulingResponse(response, True)
             
     @inlineCallbacks
@@ -334,7 +378,7 @@
         # being requested, but for now we will do one scheduling message per attendee.
 
         # Do one per attendee
-        for attendee, _ignore in self.attendeesByInstance:
+        for attendee in self.attendees:
 
             # Don't send message back to the ORGANIZER
             if attendee in self.organizerPrincipal.calendarUserAddresses():
@@ -353,7 +397,7 @@
     
             # Do the PUT processing
             log.info("Implicit REQUEST - organizer: '%s' to attendee: '%s', UID: '%s'" % (self.organizer, attendee, self.uid,))
-            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg, self.internal_request))
+            response = (yield scheduler.doSchedulingViaPUT(self.originator, (attendee,), itipmsg, self.internal_request))
             self.handleSchedulingResponse(response, True)
 
     def handleSchedulingResponse(self, response, is_organizer):
@@ -376,6 +420,10 @@
     @inlineCallbacks
     def doImplicitAttendee(self):
 
+        # Do access control
+        if not self.internal_request:
+            yield self.doAccessControl(self.attendeePrincipal, False)
+
         if self.deleting:
             #log.error("Attendee '%s' is not allowed to delete an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
             #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
@@ -390,7 +438,7 @@
             # Determine whether the current change is allowed
             if self.isAttendeeChangeInsignificant():
                 log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
-                return
+                returnValue(None)
                 
             log.debug("Implicit - attendee '%s' is updating UID: '%s'" % (self.attendee, self.uid))
             yield self.scheduleWithOrganizer()
@@ -469,6 +517,6 @@
             self.handleSchedulingResponse(response, False)
             
         log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
-        d = scheduler.doSchedulingViaPUT(self.attendee, (self.organizer,), itipmsg, self.internal_request)
+        d = scheduler.doSchedulingViaPUT(self.originator, (self.organizer,), itipmsg, self.internal_request)
         d.addCallback(_gotResponse)
         return d

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -376,7 +376,7 @@
                 instance = original.overriddenComponent(instance_rid)
                 if instance is None:
                     instance = original.masterComponent()
-            assert instance is not None
+            assert instance is not None, "Need a master component"
 
             # Add some required properties extracted from the original
             comp.addProperty(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
@@ -404,7 +404,7 @@
             # Extract the matching attendee property
             for attendee in attendees:
                 attendeeProp = instance.getAttendeeProperty((attendee,))
-                assert attendeeProp is not None
+                assert attendeeProp is not None, "Must have matching ATTENDEE property"
                 comp.addProperty(attendeeProp)
 
             tzids.update(comp.timezoneIDs())
@@ -429,6 +429,9 @@
         itip.replaceProperty(Property("PRODID", iCalendarProductID))
         itip.addProperty(Property("METHOD", "REQUEST"))
         
+        # Force update to DTSTAMP everywhere
+        itip.replacePropertyInAllComponents(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
+
         # Now filter out components that do not contain every attendee
         itip.attendeesView(attendees)
         
@@ -445,6 +448,9 @@
         itip.replaceProperty(Property("PRODID", iCalendarProductID))
         itip.addProperty(Property("METHOD", "REPLY"))
         
+        # Force update to DTSTAMP everywhere
+        itip.replacePropertyInAllComponents(Property("DTSTAMP", datetime.datetime.now(tz=utc)))
+
         # Remove all attendees except the one we want
         itip.removeAllButOneAttendee(attendee)
         

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -18,6 +18,7 @@
 import twistedcaldav.test.util
 from twistedcaldav.scheduling.implicit import ImplicitScheduler
 from dateutil.tz import tzutc
+from twisted.web2.dav import davxml
 import datetime
 
 class Implicit (twistedcaldav.test.util.TestCase):
@@ -746,8 +747,16 @@
             ),
         )
 
+        class TestResource(object):
+            def currentPrincipal(self, request):
+                return davxml.Principal(davxml.Unauthenticated)
+
+        resource = TestResource()
+
         for description, calendar1, calendar2, result in data:
             scheduler = ImplicitScheduler()
+            scheduler.resource = resource
+            scheduler.request = None
             scheduler.oldcalendar = Component.fromString(calendar1)
             scheduler.calendar = Component.fromString(calendar2)
             scheduler.extractCalendarData()

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py	2008-09-12 15:40:23 UTC (rev 2989)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -893,7 +893,9 @@
         for original, filtered, attendees in data:
             component = Component.fromString(original)
             itipped = iTipGenerator.generateAttendeeRequest(component, attendees)
-            self.assertEqual(filtered, str(itipped).replace("\r", ""))
+            itipped = str(itipped).replace("\r", "")
+            itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
+            self.assertEqual(filtered, itipped)
 
     def test_cancel(self):
         

Copied: CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py (from rev 2989, CalendarServer/branches/users/cdaboo/implicitauto-2947/twistedcaldav/test/test_memcachelock.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_memcachelock.py	2008-09-12 16:30:40 UTC (rev 2990)
@@ -0,0 +1,170 @@
+# Copyright (c) 2007 Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test the memcache client protocol.
+"""
+
+from twistedcaldav.memcache import MemCacheProtocol
+
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransportWithDisconnection
+from twisted.internet.task import Clock
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.memcachelock import MemcacheLock
+
+
+class MemCacheTestCase(TestCase):
+    """
+    Test client protocol class L{MemCacheProtocol}.
+    """
+
+    class FakedMemcacheLock(MemcacheLock):
+        
+        def __init__(self, faked, namespace, locktoken, timeout=5.0, retry_interval=0.1, expire_time=0):
+            """
+            
+            @param namespace: a unique namespace for this lock's tokens
+            @type namespace: C{str}
+            @param locktoken: the name of the locktoken
+            @type locktoken: C{str}
+            @param timeout: the maximum time in seconds that the lock should block
+            @type timeout: C{float}
+            @param retry_interval: the interval to retry acquiring the lock
+            @type retry_interval: C{float}
+            @param expiryTime: the time in seconds for the lock to expire. Zero: no expiration.
+            @type expiryTime: C{float}
+            """
+    
+            super(MemCacheTestCase.FakedMemcacheLock, self).__init__(namespace, locktoken, timeout, retry_interval, expire_time)
+            self.faked = faked
+
+        def _getMemcacheProtocol(self):
+            
+            return self.faked
+        
+    def setUp(self):
+        """
+        Create a memcache client, connect it to a string protocol, and make it
+        use a deterministic clock.
+        """
+        self.proto = MemCacheProtocol()
+        self.clock = Clock()
+        self.proto.callLater = self.clock.callLater
+        self.transport = StringTransportWithDisconnection()
+        self.transport.protocol = self.proto
+        self.proto.makeConnection(self.transport)
+
+
+    def _test(self, d, send, recv, result):
+        """
+        Shortcut method for classic tests.
+
+        @param d: the resulting deferred from the memcache command.
+        @type d: C{Deferred}
+
+        @param send: the expected data to be sent.
+        @type send: C{str}
+
+        @param recv: the data to simulate as reception.
+        @type recv: C{str}
+
+        @param result: the expected result.
+        @type result: C{any}
+        """
+        def cb(res):
+            self.assertEquals(res, result)
+        self.assertEquals(self.transport.value(), send)
+        self.transport.clear()
+        d.addCallback(cb)
+        self.proto.dataReceived(recv)
+        return d
+
+    def test_get(self):
+        """
+        L{MemCacheProtocol.get} should return a L{Deferred} which is
+        called back with the value and the flag associated with the given key
+        if the server returns a successful result.
+        """
+        lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+        return self._test(
+            lock.get("foo"),
+            "get lock:foo\r\n",
+            "VALUE lock:foo 0 3\r\nbar\r\nEND\r\n",
+            "bar"
+        )
+
+    def test_set(self):
+        """
+        L{MemCacheProtocol.get} should return a L{Deferred} which is
+        called back with the value and the flag associated with the given key
+        if the server returns a successful result.
+        """
+        lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+        return self._test(
+            lock.set("foo", "bar"),
+            "set lock:foo 0 0 3\r\nbar\r\n",
+            "STORED\r\n",
+            True
+        )
+
+    @inlineCallbacks
+    def test_acquire(self):
+        """
+        L{MemCacheProtocol.get} should return a L{Deferred} which is
+        called back with the value and the flag associated with the given key
+        if the server returns a successful result.
+        """
+        lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+        yield self._test(
+            lock.acquire(),
+            "add lock:locking 0 0 1\r\n1\r\n",
+            "STORED\r\n",
+            True
+        )
+        self.assertTrue(lock._hasLock)
+
+    @inlineCallbacks
+    def test_acquire_release(self):
+        """
+        L{MemCacheProtocol.get} should return a L{Deferred} which is
+        called back with the value and the flag associated with the given key
+        if the server returns a successful result.
+        """
+        lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+        yield self._test(
+            lock.acquire(),
+            "add lock:locking 0 0 1\r\n1\r\n",
+            "STORED\r\n",
+            True
+        )
+        self.assertTrue(lock._hasLock)
+        yield self._test(
+            lock.release(),
+            "delete lock:locking\r\n",
+            "DELETED\r\n",
+            True
+        )
+        self.assertFalse(lock._hasLock)
+
+    @inlineCallbacks
+    def test_acquire_clean(self):
+        """
+        L{MemCacheProtocol.get} should return a L{Deferred} which is
+        called back with the value and the flag associated with the given key
+        if the server returns a successful result.
+        """
+        lock = MemCacheTestCase.FakedMemcacheLock(self.proto, "lock", "locking")
+        yield self._test(
+            lock.acquire(),
+            "add lock:locking 0 0 1\r\n1\r\n",
+            "STORED\r\n",
+            True
+        )
+        yield self._test(
+            lock.clean(),
+            "delete lock:locking\r\n",
+            "DELETED\r\n",
+            True
+        )
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080912/b6d2cca6/attachment-0001.html 


More information about the calendarserver-changes mailing list