[CalendarServer-changes] [2735] CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Sun Jul 20 17:04:24 PDT 2008


Revision: 2735
          http://trac.macosforge.org/projects/calendarserver/changeset/2735
Author:   cdaboo at apple.com
Date:     2008-07-20 17:04:23 -0700 (Sun, 20 Jul 2008)
Log Message:
-----------
Organizer's implicit PUT behavior. Basically working, but some complicated stuff still to do.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/itip.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/implicit.py

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/itip.py	2008-07-19 17:27:36 UTC (rev 2734)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/itip.py	2008-07-21 00:04:23 UTC (rev 2735)
@@ -47,7 +47,6 @@
 from twistedcaldav.log import Logger
 from twistedcaldav.ical import Property, iCalendarProductID, Component
 from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.resource import isCalendarCollectionResource
 
 from vobject.icalendar import utc
@@ -58,6 +57,7 @@
 
 __all__ = [
     "iTipProcessor",
+    "iTipGenerator",
 ]
 
 class iTipException(Exception):
@@ -577,6 +577,7 @@
             itipper = False
         
         # Now write it to the resource
+        from twistedcaldav.method.put_common import StoreCalendarObjectResource
         yield StoreCalendarObjectResource(
                      request=self.request,
                      destination = newchild,
@@ -905,6 +906,7 @@
     def generateCancel(original, attendees, instances=None):
         
         itip = Component("VCALENDAR")
+        itip.addProperty(Property("VERSION", "2.0"))
         itip.addProperty(Property("PRODID", iCalendarProductID))
         itip.addProperty(Property("METHOD", "CANCEL"))
 

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/method/put_common.py	2008-07-19 17:27:36 UTC (rev 2734)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/method/put_common.py	2008-07-21 00:04:23 UTC (rev 2735)
@@ -54,6 +54,7 @@
 from twistedcaldav.index import ReservationError
 from twistedcaldav.instance import TooManyInstancesError
 from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.implicit import ImplicitScheduler
 
 log = Logger()
 
@@ -674,10 +675,15 @@
             
             # Get current quota state.
             yield self.checkQuota()
-    
+
+            # Do scheduling
+            if not self.isiTIP:
+                scheduler = ImplicitScheduler()
+                self.calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar))
+
             # Initialize the rollback system
             self.setupRollback()
-        
+
             """
             Handle actual store operations here.
             

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py	2008-07-19 17:27:36 UTC (rev 2734)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py	2008-07-21 00:04:23 UTC (rev 2735)
@@ -20,6 +20,7 @@
     "cuaddress",
     "delivery",
     "imip",
+    "implicit",
     "ischedule",
     "scheduler",
 ]

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py	2008-07-19 17:27:36 UTC (rev 2734)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py	2008-07-21 00:04:23 UTC (rev 2735)
@@ -33,7 +33,6 @@
 from twistedcaldav.itip import iTipProcessor
 from twistedcaldav.log import Logger
 from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.resource import isCalendarCollectionResource
 from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
     RemoteCalendarUser
@@ -148,6 +147,7 @@
 
         # Copy calendar to inbox (doing fan-out)
         try:
+            from twistedcaldav.method.put_common import StoreCalendarObjectResource
             yield StoreCalendarObjectResource(
                          request=self.scheduler.request,
                          destination = child,

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/implicit.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/implicit.py	2008-07-21 00:04:23 UTC (rev 2735)
@@ -0,0 +1,237 @@
+#
+# Copyright (c) 2005-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 twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.web2 import responsecode
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.http import HTTPError
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.itip import iTipGenerator
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+
+__all__ = [
+    "ImplicitScheduler",
+]
+
+log = Logger()
+
+class ImplicitScheduler(object):
+    
+    def __init__(self):
+        pass
+
+    @inlineCallbacks
+    def doImplicitScheduling(self, request, resource, calendar):
+        """
+        Do implicit scheduling operation based on the calendar data that is being PUT
+
+        @param request:
+        @type request:
+        @param resource:
+        @type resource:
+        @param calendar:
+        @type calendar:
+        
+        @return: a new calendar object modified with scheduling information
+        """
+        
+        self.request = request
+        self.resource = resource
+        self.calendar = calendar
+        self.calendar_owner = (yield self.resource.owner(self.request))
+
+        
+        # Get some useful information from the calendar
+        self.extractCalendarData()
+
+        # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
+        if self.isOrganizerScheduling():
+            yield self.doImplicitOrganizer()
+        elif self.isAttendeeScheduling():
+            yield self.doImplicitAttendee()
+
+        returnValue(self.calendar)
+
+    def extractCalendarData(self):
+        
+        # Get the ORGANIZER and verify it is the same for all components
+        organizers = self.calendar.getOrganizersByInstance()
+        self.organizer = None
+        for organizer, _ignore in organizers:
+            if self.organizer:
+                if organizer != self.organizer:
+                    # We have different ORGANIZERs in the same iCalendar object - this is an error
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "single-organizer")))
+            else:
+                self.organizer = organizer
+        
+        # Get the ATTENDEEs
+        self.attendeesByInstance = self.calendar.getAttendeesByInstance()
+        self.attendees = set()
+        for attendee in self.attendeesByInstance:
+            self.attendees.add(attendee)
+    
+    def isOrganizerScheduling(self):
+        """
+        Test whether this is a scheduling operation by an organizer
+        """
+        
+        # First must have organizer property
+        if not self.organizer:
+            return False
+        
+        # Organizer must map to a valid principal
+        self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
+        if not self.organizerPrincipal:
+            return False
+        
+        # Organizer must be the owner of the calendar resource
+        if str(self.calendar_owner) != self.organizerPrincipal.principalURL():
+            return False
+
+        return True
+
+    def isAttendeeScheduling(self):
+        
+        # First must have organizer property
+        if not self.organizer:
+            return False
+        
+        # Check to see whether any attendee is the owner
+        for attendee in self.attendees:
+            attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+            if attendeePrincipal and attendeePrincipal.principalURL() == str(self.calendar_owner):
+                self.attendeePrincipal = attendeePrincipal
+                return True
+        
+        return False
+
+    @inlineCallbacks
+    def doImplicitOrganizer(self):
+        
+        # Check for a new resource or an update
+        if self.resource.exists():
+            
+            # Read in existing data
+            self.oldcalendar = self.resource.iCalendar()
+            
+            # Significant change
+            if not self.isChangeSignificant():
+                # Nothing to do
+                return
+            
+            # Check for removed attendees
+            self.findRemovedAttendees()
+        else:
+            self.oldcalendar = None
+            self.cancelledAttendees = ()   
+            
+        yield self.scheduleAttendees()
+
+    def isChangeSignificant(self):
+        
+        # TODO: diff two calendars and see what happened. For now treat any change as significant.
+        return True
+    
+    def findRemovedAttendees(self):
+        """
+        Look for attendees that have been removed from any instances. Save those off
+        as users that need to be sent a cancel.
+        """
+        
+        oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance()
+        
+        mappedOld = set(oldAttendeesByInstance)
+        mappedNew = set(self.attendeesByInstance)
+        
+        self.cancelledAttendees = mappedOld.difference(mappedNew)
+
+    @inlineCallbacks
+    def scheduleAttendees(self):
+        
+        # First process cancelled attendees
+        yield self.processCancels()
+        
+        # Process regular requests next
+        yield self.processRequests()
+
+    @inlineCallbacks
+    def processCancels(self):
+        
+        # TODO: a better policy here is to aggregate by attendees with the same set of instances
+        # being cancelled, but for now we will do one scheduling message per attendee.
+
+        # Do one per attendee
+        aggregated = {}
+        for attendee, rid in self.cancelledAttendees:
+            aggregated.setdefault(attendee, []).append(rid)
+            
+        for attendee, rids in aggregated.iteritems():
+            
+            # Don't send message back to the ORGANIZER
+            if attendee in self.organizerPrincipal.calendarUserAddresses():
+                continue
+
+            # Generate an iTIP CANCEL message for this attendee, cancelling
+            # each instance or the whole
+            
+            if None in rids:
+                # One big CANCEL will do
+                itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None)
+            else:
+                # Multiple CANCELs
+                itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), rids)
+
+            # Send scheduling message
+            
+            # This is a local CALDAV scheduling operation.
+            scheduler = CalDAVScheduler(self.request, self.resource)
+    
+            # Do the PUT processing
+            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg))
+            
+            # TODO: need to figure out how to process the response for a CANCEL
+            returnValue(response)
+            
+    @inlineCallbacks
+    def processRequests(self):
+        
+        # TODO: a better policy here is to aggregate by attendees with the same set of instances
+        # being requested, but for now we will do one scheduling message per attendee.
+
+        # Do one per attendee
+        for attendee, _ignore in self.attendeesByInstance:
+
+            # Don't send message back to the ORGANIZER
+            if attendee in self.organizerPrincipal.calendarUserAddresses():
+                continue
+
+            itipmsg = iTipGenerator.generateAttendeeRequest(self.calendar, (attendee,))
+
+            # Send scheduling message
+
+            # This is a local CALDAV scheduling operation.
+            scheduler = CalDAVScheduler(self.request, self.resource)
+    
+            # Do the PUT processing
+            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg))
+            
+            # TODO: need to figure out how to process the response for a REQUEST
+            returnValue(response)
+            
+    def doImplicitAttendee(self):
+        pass


Property changes on: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/implicit.py
___________________________________________________________________
Name: svn:executable
   + *

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py	2008-07-19 17:27:36 UTC (rev 2734)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py	2008-07-21 00:04:23 UTC (rev 2735)
@@ -68,6 +68,7 @@
         self.timeRange = None
         self.excludeUID = None
         self.fakeTheResult = False
+        self.method = "Unknown"
     
     @inlineCallbacks
     def doSchedulingViaPOST(self):
@@ -75,6 +76,8 @@
         The Scheduling POST operation on an Outbox.
         """
     
+        self.method = "POST"
+
         # Do some extra authorization checks
         self.checkAuthorization()
 
@@ -92,6 +95,8 @@
         The implicit scheduling PUT operation.
         """
     
+        self.method = "PUT"
+
         # Do some extra authorization checks
         self.checkAuthorization()
 
@@ -135,7 +140,7 @@
         # Must have Originator header
         originator = self.request.headers.getRawHeaders("originator")
         if originator is None or (len(originator) != 1):
-            log.err("POST request must have Originator header")
+            log.err("%s request must have Originator header" % (self.method,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
         else:
             self.originator = originator[0]
@@ -144,7 +149,7 @@
         # Get list of Recipient headers
         rawRecipients = self.request.headers.getRawHeaders("recipient")
         if rawRecipients is None or (len(rawRecipients) == 0):
-            log.err("POST request must have at least one Recipient header")
+            log.err("%s request must have at least one Recipient header" % (self.method,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
     
         # Recipient header may be comma separated list
@@ -168,7 +173,7 @@
             self.calendar = (yield Component.fromIStream(self.request.stream))
         except:
             # FIXME: Bare except
-            log.err("Error while handling POST: %s" % (Failure(),))
+            log.err("Error while handling %s: %s" % (self.method, Failure(),))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
 
     def checkAuthorization(self):
@@ -193,23 +198,23 @@
         # Must be a valid calendar
         try:
             self.calendar.validCalendarForCalDAV()
-        except ValueError:
-            log.err("POST request calendar component is not valid: %s" % (self.calendar,))
+        except ValueError, e:
+            log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendar,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
     
         # Must have a METHOD
         if not self.calendar.isValidMethod():
-            log.err("POST request must have valid METHOD property in calendar component: %s" % (self.calendar,))
+            log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendar,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
         
         # Verify iTIP behavior
         if not self.calendar.isValidITIP():
-            log.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (self.calendar,))
+            log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, self.calendar,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
 
         # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
         if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
-            log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (self.calendar,))
+            log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendar,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
     
     def checkForFreeBusy(self):
@@ -279,7 +284,7 @@
         freebusy = self.checkForFreeBusy()
 
         # Prepare for multiple responses
-        responses = ScheduleResponseQueue("POST", responsecode.OK)
+        responses = ScheduleResponseQueue(self.method, responsecode.OK)
     
         # Loop over each recipient and aggregate into lists by service types.
         caldav_recipients = []
@@ -466,7 +471,7 @@
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
 
         # Make sure that the ORGANIZER's Outbox is the request URI
-        if self.organizer.principal.scheduleOutboxURL() != self.request.uri:
+        if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
             log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080720/ebab6ed3/attachment-0001.html 


More information about the calendarserver-changes mailing list