[CalendarServer-changes] [8642] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Feb 9 13:34:19 PST 2012


Revision: 8642
          http://trac.macosforge.org/projects/calendarserver/changeset/8642
Author:   cdaboo at apple.com
Date:     2012-02-09 13:34:19 -0800 (Thu, 09 Feb 2012)
Log Message:
-----------
Add new auto-schedule-modes to allow more control over how auto-scheduling occurs by default or a per-calendar user basis.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tools/principals.py
    CalendarServer/trunk/calendarserver/tools/test/test_principals.py
    CalendarServer/trunk/calendarserver/webadmin/resource.py
    CalendarServer/trunk/calendarserver/webadmin/template.html
    CalendarServer/trunk/calendarserver/webadmin/test/test_resource.py
    CalendarServer/trunk/conf/auth/augments-test.xml
    CalendarServer/trunk/conf/auth/augments.dtd
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/doc/calendarserver_manage_principals.8
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/directory/augment.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml
    CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml
    CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
    CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/processing.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py

Modified: CalendarServer/trunk/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/principals.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/calendarserver/tools/principals.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 
 ##
-# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2012 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.
@@ -37,6 +37,7 @@
 from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryError
 
 from calendarserver.tools.util import loadConfig, getDirectory, setupMemcached,  booleanArgument, checkDirectory
+from twistedcaldav.directory.augment import allowedAutoScheduleModes
 
 __all__ = [
     "principalForPrincipalID", "proxySubprincipal", "addProxy", "removeProxy",
@@ -83,6 +84,8 @@
     print "  --remove-proxy=principal: remove a proxy"
     print "  --set-auto-schedule={true|false}: set auto-accept state"
     print "  --get-auto-schedule: read auto-schedule state"
+    print "  --set-auto-schedule-mode={default|none|accept-always|decline-always|accept-if-free|decline-if-busy|automatic}: set auto-schedule mode"
+    print "  --get-auto-schedule-mode: read auto-schedule mode"
     print "  --add {locations|resources} 'full name' [record name] [GUID]: add a principal"
     print "  --remove: remove a principal"
 
@@ -111,6 +114,8 @@
                 "remove-proxy=",
                 "set-auto-schedule=",
                 "get-auto-schedule",
+                "set-auto-schedule-mode=",
+                "get-auto-schedule-mode",
                 "verbose",
             ],
         )
@@ -203,6 +208,19 @@
         elif opt in ("", "--get-auto-schedule"):
             principalActions.append((action_getAutoSchedule,))
 
+        elif opt in ("", "--set-auto-schedule-mode"):
+            try:
+                if arg not in allowedAutoScheduleModes:
+                    raise ValueError("Unknown auto-schedule mode: %s" % (arg,))
+                autoScheduleMode = arg
+            except ValueError, e:
+                abort(e)
+
+            principalActions.append((action_setAutoScheduleMode, autoScheduleMode))
+
+        elif opt in ("", "--get-auto-schedule-mode"):
+            principalActions.append((action_getAutoScheduleMode,))
+
         else:
             raise NotImplementedError(opt)
 
@@ -677,7 +695,7 @@
     if principal.record.recordType == "groups":
         print "Enabling auto-schedule for %s is not allowed." % (principal,)
 
-    elif principal.record.recordType == "users" and not config.Scheduling.Options.AllowUserAutoAccept:
+    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
         print "Enabling auto-schedule for %s is not allowed." % (principal,)
 
     else:
@@ -697,12 +715,44 @@
 
 def action_getAutoSchedule(principal):
     autoSchedule = principal.getAutoSchedule()
-    print "Autoschedule for %s is %s" % (
+    print "Auto-schedule for %s is %s" % (
         prettyPrincipal(principal),
         { True: "true", False: "false" }[autoSchedule],
     )
 
+ at inlineCallbacks
+def action_setAutoScheduleMode(principal, autoScheduleMode):
+    if principal.record.recordType == "groups":
+        print "Setting auto-schedule mode for %s is not allowed." % (principal,)
 
+    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
+        print "Setting auto-schedule mode for %s is not allowed." % (principal,)
+
+    else:
+        print "Setting auto-schedule mode to %s for %s" % (
+            autoScheduleMode,
+            prettyPrincipal(principal),
+        )
+
+        (yield updateRecord(False, config.directory,
+            principal.record.recordType,
+            guid=principal.record.guid,
+            shortNames=principal.record.shortNames,
+            fullName=principal.record.fullName,
+            autoScheduleMode=autoScheduleMode,
+            **principal.record.extras
+        ))
+
+def action_getAutoScheduleMode(principal):
+    autoScheduleMode = principal.getAutoScheduleMode()
+    if not autoScheduleMode:
+        autoScheduleMode = "automatic"
+    print "Auto-schedule mode for %s is %s" % (
+        prettyPrincipal(principal),
+        autoScheduleMode,
+    )
+
+
 def abort(msg, status=1):
     sys.stdout.write("%s\n" % (msg,))
     try:
@@ -796,6 +846,12 @@
     else:
         autoSchedule = recordType in ("locations", "resources")
 
+    if kwargs.has_key("autoScheduleMode"):
+        autoScheduleMode = kwargs["autoScheduleMode"]
+        del kwargs["autoScheduleMode"]
+    else:
+        autoScheduleMode = None
+
     for key, value in kwargs.items():
         if isinstance(value, unicode):
             kwargs[key] = value.encode("utf-8")
@@ -819,6 +875,7 @@
     augmentService = directory.serviceForRecordType(recordType).augmentService
     augmentRecord = (yield augmentService.getAugmentRecord(kwargs['guid'], recordType))
     augmentRecord.autoSchedule = autoSchedule
+    augmentRecord.autoScheduleMode = autoScheduleMode
     (yield augmentService.addAugmentRecords([augmentRecord]))
     try:
         directory.updateRecord(recordType, **kwargs)

Modified: CalendarServer/trunk/calendarserver/tools/test/test_principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_principals.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/calendarserver/tools/test/test_principals.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2012 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.
@@ -25,7 +25,8 @@
 from twistedcaldav.directory.directory import DirectoryError
 from twistedcaldav.directory import calendaruserproxy
 
-from twistedcaldav.test.util import TestCase, CapturingProcessProtocol
+from twistedcaldav.test.util import TestCase, CapturingProcessProtocol,\
+    ErrorOutput
 
 from calendarserver.tap.util import directoryFromConfig
 from calendarserver.tools.principals import parseCreationArgs, matchStrings, updateRecord, principalForPrincipalID, getProxies, setProxies
@@ -130,8 +131,12 @@
 
         results = yield self.runCommand("--get-auto-schedule",
             "resources:newresource")
-        self.assertTrue(results.startswith('Autoschedule for "New Resource" (resources:newresource) is true'))
+        self.assertTrue(results.startswith('Auto-schedule for "New Resource" (resources:newresource) is true'))
 
+        results = yield self.runCommand("--get-auto-schedule-mode",
+            "resources:newresource")
+        self.assertTrue(results.startswith('Auto-schedule mode for "New Resource" (resources:newresource) is default'))
+
         results = yield self.runCommand("--list-principals=resources")
         self.assertTrue("newresource" in results)
 
@@ -220,7 +225,7 @@
     def test_autoSchedule(self):
         results = yield self.runCommand("--get-auto-schedule",
             "locations:location01")
-        self.assertTrue(results.startswith('Autoschedule for "Room 01" (locations:location01) is false'))
+        self.assertTrue(results.startswith('Auto-schedule for "Room 01" (locations:location01) is false'))
 
         results = yield self.runCommand("--set-auto-schedule=true",
             "locations:location01")
@@ -228,7 +233,7 @@
 
         results = yield self.runCommand("--get-auto-schedule",
             "locations:location01")
-        self.assertTrue(results.startswith('Autoschedule for "Room 01" (locations:location01) is true'))
+        self.assertTrue(results.startswith('Auto-schedule for "Room 01" (locations:location01) is true'))
 
         results = yield self.runCommand("--set-auto-schedule=true",
             "users:user01")
@@ -236,6 +241,33 @@
 
 
     @inlineCallbacks
+    def test_autoScheduleMode(self):
+        results = yield self.runCommand("--get-auto-schedule-mode",
+            "locations:location01")
+        self.assertTrue(results.startswith('Auto-schedule mode for "Room 01" (locations:location01) is default'))
+
+        results = yield self.runCommand("--set-auto-schedule-mode=accept-if-free",
+            "locations:location01")
+        self.assertTrue(results.startswith('Setting auto-schedule mode to accept-if-free for "Room 01" (locations:location01)'))
+
+        results = yield self.runCommand("--get-auto-schedule-mode",
+            "locations:location01")
+        self.assertTrue(results.startswith('Auto-schedule mode for "Room 01" (locations:location01) is accept-if-free'))
+
+        results = yield self.runCommand("--set-auto-schedule-mode=decline-if-busy",
+            "users:user01")
+        self.assertTrue(results.startswith('Setting auto-schedule mode for (users)user01 is not allowed.'))
+
+        try:
+            results = yield self.runCommand("--set-auto-schedule-mode=bogus",
+                "users:user01")
+        except ErrorOutput:
+            pass
+        else:
+            self.fail("Expected command failure")
+
+
+    @inlineCallbacks
     def test_updateRecord(self):
         directory = directoryFromConfig(config)
         guid = "EEE28807-A8C5-46C8-A558-A08281C558A7"

Modified: CalendarServer/trunk/calendarserver/webadmin/resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/resource.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/calendarserver/webadmin/resource.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,6 +1,6 @@
 # -*- test-case-name: calendarserver.webadmin.test.test_resource -*-
 ##
-# Copyright (c) 2009-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2012 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.
@@ -267,7 +267,7 @@
         if (self.principalResource.record.recordType != "users" and
             self.principalResource.record.recordType != "groups" or
             self.principalResource.record.recordType == "users" and
-            config.Scheduling.Options.AllowUserAutoAccept):
+            config.Scheduling.Options.AutoSchedule.AllowUsers):
             return tag
         return ""
 
@@ -294,6 +294,83 @@
         return tag
 
 
+    @renderer
+    def autoScheduleModeDefault(self, request, tag):
+        """
+        Renderer which sets the 'selected' attribute on its tag based on the resource
+        auto-schedule-mode.
+        """
+        if self.principalResource.getAutoScheduleMode() == "default":
+            tag(selected='selected')
+        return tag
+
+
+    @renderer
+    def autoScheduleModeNone(self, request, tag):
+        """
+        Renderer which sets the 'selected' attribute on its tag based on the resource
+        auto-schedule-mode.
+        """
+        if self.principalResource.getAutoScheduleMode() == "none":
+            tag(selected='selected')
+        return tag
+
+
+    @renderer
+    def autoScheduleModeAcceptAlways(self, request, tag):
+        """
+        Renderer which sets the 'selected' attribute on its tag based on the resource
+        auto-schedule-mode.
+        """
+        if self.principalResource.getAutoScheduleMode() == "accept-always":
+            tag(selected='selected')
+        return tag
+
+
+    @renderer
+    def autoScheduleModeDeclineAlways(self, request, tag):
+        """
+        Renderer which sets the 'selected' attribute on its tag based on the resource
+        auto-schedule-mode.
+        """
+        if self.principalResource.getAutoScheduleMode() == "decline-always":
+            tag(selected='selected')
+        return tag
+
+
+    @renderer
+    def autoScheduleModeAcceptIfFree(self, request, tag):
+        """
+        Renderer which sets the 'selected' attribute on its tag based on the resource
+        auto-schedule-mode.
+        """
+        if self.principalResource.getAutoScheduleMode() == "accept-if-free":
+            tag(selected='selected')
+        return tag
+
+
+    @renderer
+    def autoScheduleModeDeclineIfBusy(self, request, tag):
+        """
+        Renderer which sets the 'selected' attribute on its tag based on the resource
+        auto-schedule-mode.
+        """
+        if self.principalResource.getAutoScheduleMode() == "decline-if-busy":
+            tag(selected='selected')
+        return tag
+
+
+    @renderer
+    def autoScheduleModeAutomatic(self, request, tag):
+        """
+        Renderer which sets the 'selected' attribute on its tag based on the resource
+        auto-schedule-mode.
+        """
+        if self.principalResource.getAutoScheduleMode() == "automatic" or not self.principalResource.getAutoScheduleMode():
+            tag(selected='selected')
+        return tag
+
+
     _matrix = None
 
     @inlineCallbacks
@@ -547,6 +624,7 @@
             return matches
 
         autoSchedule = queryValue("autoSchedule")
+        autoScheduleMode = queryValue("autoScheduleMode")
         makeReadProxies = queryValues("mkReadProxy|")
         makeWriteProxies = queryValues("mkWriteProxy|")
         removeProxies = queryValues("rmProxy|")
@@ -557,8 +635,9 @@
             if ( principal.record.recordType != "users" and
                  principal.record.recordType != "groups" or
                  principal.record.recordType == "users" and
-                 config.Scheduling.Options.AllowUserAutoAccept):
+                 config.Scheduling.Options.AutoSchedule.AllowUsers):
                 (yield principal.setAutoSchedule(autoSchedule == "true"))
+                (yield principal.setAutoScheduleMode(autoScheduleMode))
 
         # Update the proxies if specified.
         for proxyId in removeProxies:

Modified: CalendarServer/trunk/calendarserver/webadmin/template.html
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/template.html	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/calendarserver/webadmin/template.html	2012-02-09 21:34:19 UTC (rev 8642)
@@ -128,6 +128,18 @@
             <option t:render="isAutoSchedule" value="true">Yes</option>
             <option t:render="isntAutoSchedule" value="false">No</option>
           </select>
+          <br/>
+          Auto-Schedule Mode
+          <select id="sel_autoScheduleMode" name="autoScheduleMode">
+            <option t:render="autoScheduleModeDefault" value="default">Default</option>
+            <option t:render="autoScheduleModeNone" value="none">None</option>
+            <option t:render="autoScheduleModeAcceptAlways" value="accept-always">Accept Always</option>
+            <option t:render="autoScheduleModeDeclineAlways" value="decline-always">Decline Always</option>
+            <option t:render="autoScheduleModeAcceptIfFree" value="accept-if-free">Accept If Free</option>
+            <option t:render="autoScheduleModeDeclineIfBusy" value="decline-if-busy">Decline If Busy</option>
+            <option t:render="autoScheduleModeAutomatic" value="automatic">Automatic (Accept and Decline)</option>
+          </select>
+          <br/>
           <input type="submit" value="Change" />
         </div>
       </form>

Modified: CalendarServer/trunk/calendarserver/webadmin/test/test_resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/test/test_resource.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/calendarserver/webadmin/test/test_resource.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2011 Apple Inc. All rights reserved.
+# Copyright (c) 2011-2012 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.
@@ -335,6 +335,38 @@
 
 
     @inlineCallbacks
+    def test_autoScheduleModeMenu(self):
+        """
+        When rendering a resource, an "Auto-Schedule Mode" menu with various options
+        should be displayed.
+        """
+        
+        modes = ("default", "none", "accept-always", "decline-always", "accept-if-free", "decline-if-busy", "automatic",)
+    
+        for ctr, expectValue in enumerate(modes):
+
+            self.resource.getResourceById = partial(FakePrincipalResource, self,
+                                                    recordType='resources',
+                                                    autosched=True,
+                                                    autoschedmode=expectValue)
+            document = yield self.renderPage(dict(resourceId=["qux"]))
+            autoScheduleModeMenu = document.getElementById("sel_autoScheduleMode")
+            self.assertEquals(autoScheduleModeMenu.getAttribute("name"),
+                              "autoScheduleMode")
+
+            popup = getElementsByTagName(autoScheduleModeMenu, 'option')
+
+            # Sanity checks to make sure we got the right items
+            for i, mode in enumerate(modes):
+                self.assertEquals(popup[i].getAttribute("value"), mode)
+
+            popupValues = [popup[i] for i in range(len(modes))]
+            for i in range(len(modes)):
+                self.assertEquals(popupValues[i].hasAttribute("selected"), ctr == i)
+            self.assertEquals(popupValues[ctr].getAttribute("selected"), "selected")
+
+
+    @inlineCallbacks
     def test_proxiesListing(self):
         """
         Resource principals will have their proxies listed in a table.
@@ -403,11 +435,13 @@
 
 
 class FakePrincipalResource(object):
-    def __init__(self, test, req=None, resid='no-id-given', autosched=True,
+    def __init__(self, test, req=None, resid='no-id-given',
+                 autosched=True, autoschedmode=None,
                  recordType="users", extraProperties=(), hasProxies=True):
         self.test = test
         self.resid = resid
         self.autosched = autosched
+        self.autoschedmode = autoschedmode
         self.recordType = recordType
         self.extraProperties = extraProperties
         self.hasProxies = hasProxies
@@ -469,3 +503,7 @@
         return self.autosched
 
 
+    def getAutoScheduleMode(self):
+        return self.autoschedmode
+
+

Modified: CalendarServer/trunk/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/trunk/conf/auth/augments-test.xml	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/conf/auth/augments-test.xml	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+Copyright (c) 2009-2012 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.
@@ -32,13 +32,61 @@
     <enable-addressbook>true</enable-addressbook>
     <auto-schedule>true</auto-schedule>
   </record>
-  <record repeat="10">
+  <record repeat="4">
     <uid>resource%02d</uid>
     <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
     <auto-schedule>true</auto-schedule>
   </record>
+  <record>
+    <uid>resource05</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>none</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource06</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>accept-always</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource07</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>decline-always</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource08</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>accept-if-free</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource09</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>decline-if-busy</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource10</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+  </record>
   <record repeat="10">
     <uid>group%02d</uid>
     <enable>true</enable>

Modified: CalendarServer/trunk/conf/auth/augments.dtd
===================================================================
--- CalendarServer/trunk/conf/auth/augments.dtd	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/conf/auth/augments.dtd	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 <!--
-Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+Copyright (c) 2009-2012 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.
@@ -16,7 +16,7 @@
 
 <!ELEMENT augments (record*) >
 
-  <!ELEMENT record (uid, enable, (server-id, partition-id?)?, enable-calendar?, enable-addressbook?, auto-schedule?)>
+  <!ELEMENT record (uid, enable, (server-id, partition-id?)?, enable-calendar?, enable-addressbook?, auto-schedule?, auto-schedule-mode?)>
     <!ATTLIST record repeat CDATA "1">
 
   <!ELEMENT uid                (#PCDATA)>
@@ -26,4 +26,5 @@
   <!ELEMENT enable-calendar    (#PCDATA)>
   <!ELEMENT enable-addressbook (#PCDATA)>
   <!ELEMENT auto-schedule      (#PCDATA)>
+  <!ELEMENT auto-schedule-mode (#PCDATA)>
 >

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
-    Copyright (c) 2006-2011 Apple Inc. All rights reserved.
+    Copyright (c) 2006-2012 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.
@@ -815,8 +815,24 @@
         <false/>
         <key>AttendeeRefreshBatch</key>
         <integer>0</integer>
+
+		<key>AutoSchedule</key>
+		<dict>
+			<key>Enabled</key>
+			<true/>
+			<key>Always</key>
+			<false/>
+			<!-- Default mode for auto-schedule processing, one of:
+	              "none"            - no auto-scheduling
+	              "accept-always"   - always accept, ignore busy time
+	              "decline-always"  - always decline, ignore free time
+	              "accept-if-free"  - accept if free, do nothing if busy
+	              "decline-if-busy" - decline if busy, do nothing if free
+	              "automatic"       - accept if free, decline if busy -->
+	        <key>DefaultMode</key>
+	        <string>automatic</string>
+		</dict>
       </dict>
-
     </dict>
 
 

Modified: CalendarServer/trunk/doc/calendarserver_manage_principals.8
===================================================================
--- CalendarServer/trunk/doc/calendarserver_manage_principals.8	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/doc/calendarserver_manage_principals.8	2012-02-09 21:34:19 UTC (rev 8642)
@@ -36,6 +36,8 @@
 .Op Fl -remove-proxy Ar principal
 .Op Fl -set-auto-schedule Ar true|false
 .Op Fl -get-auto-schedule
+.Op Fl -set-auto-schedule-mode Ar none|accept-always|decline-always|accept-if-free|decline-if-busy|automatic
+.Op Fl -get-auto-schedule-mode
 .Op Fl -add Ar locations|resources full-name [record-name] [GUID]
 .Op Fl -remove
 .Ar principal

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2012 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.
@@ -674,6 +674,13 @@
     namespace = calendarserver_namespace
     name = "auto-schedule"
 
+class AutoScheduleMode (davxml.WebDAVTextElement):
+    """
+    The principal's auto-schedule mode
+    """
+    namespace = calendarserver_namespace
+    name = "auto-schedule-mode"
+
 ##
 # Sharing
 ##

Modified: CalendarServer/trunk/twistedcaldav/directory/augment.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/augment.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/augment.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2012 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.
@@ -35,6 +35,16 @@
 
 log = Logger()
 
+allowedAutoScheduleModes = frozenset((
+    "default",
+    "none",
+    "accept-always",
+    "decline-always",
+    "accept-if-free",
+    "decline-if-busy",
+    "automatic",
+))
+
 class AugmentRecord(object):
     """
     Augmented directory record information
@@ -48,6 +58,7 @@
         partitionID="",
         enabledForCalendaring=False,
         autoSchedule=False,
+        autoScheduleMode="default",
         enabledForAddressBooks=False,
         enabledForLogin=True,
     ):
@@ -59,6 +70,7 @@
         self.enabledForAddressBooks = enabledForAddressBooks
         self.enabledForLogin = enabledForLogin
         self.autoSchedule = autoSchedule
+        self.autoScheduleMode = autoScheduleMode if autoScheduleMode in allowedAutoScheduleModes else "default"
         self.clonedFromDefault = False
 
 recordTypesMap = {
@@ -417,6 +429,8 @@
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLEADDRESSBOOK, "true" if record.enabledForAddressBooks else "false")
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLELOGIN, "true" if record.enabledForLogin else "false")
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if record.autoSchedule else "false")
+        if record.autoScheduleMode:
+            addSubElement(recordNode, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE_MODE, record.autoScheduleMode)
 
     def refresh(self):
         """
@@ -496,7 +510,7 @@
     DBAPI based augment database implementation.
     """
 
-    schema_version = "1"
+    schema_version = "2"
     schema_type    = "AugmentDB"
     
     def __init__(self, dbID, dbapiName, dbapiArgs, **kwargs):
@@ -528,11 +542,11 @@
         """
         
         # Query for the record information
-        results = (yield self.query("select UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED from AUGMENTS where UID = :1", (uid,)))
+        results = (yield self.query("select UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, AUTOSCHEDULEMODE, LOGINENABLED from AUGMENTS where UID = :1", (uid,)))
         if not results:
             returnValue(None)
         else:
-            uid, enabled, serverid, partitionid, enabledForCalendaring, enabledForAddressBooks, autoSchedule, enabledForLogin = results[0]
+            uid, enabled, serverid, partitionid, enabledForCalendaring, enabledForAddressBooks, autoSchedule, autoScheduleMode, enabledForLogin = results[0]
             
             record = AugmentRecord(
                 uid = uid,
@@ -543,6 +557,7 @@
                 enabledForAddressBooks = enabledForAddressBooks == "T",
                 enabledForLogin = enabledForLogin == "T",
                 autoSchedule = autoSchedule == "T",
+                autoScheduleMode = autoScheduleMode,
             )
             
             returnValue(record)
@@ -597,14 +612,15 @@
         yield self._create_table(
             "AUGMENTS",
             (
-                ("UID",          "text unique"),
-                ("ENABLED",      "text(1)"),
-                ("SERVERID",     "text"),
-                ("PARTITIONID",  "text"),
-                ("CALENDARING",  "text(1)"),
-                ("ADDRESSBOOKS", "text(1)"),
-                ("AUTOSCHEDULE", "text(1)"),
-                ("LOGINENABLED", "text(1)"),
+                ("UID",              "text unique"),
+                ("ENABLED",          "text(1)"),
+                ("SERVERID",         "text"),
+                ("PARTITIONID",      "text"),
+                ("CALENDARING",      "text(1)"),
+                ("ADDRESSBOOKS",     "text(1)"),
+                ("AUTOSCHEDULE",     "text(1)"),
+                ("AUTOSCHEDULEMODE", "text"),
+                ("LOGINENABLED",     "text(1)"),
             ),
             ifnotexists=True,
         )
@@ -627,8 +643,8 @@
     def _addRecord(self, record):
         yield self.execute(
             """insert or replace into AUGMENTS
-            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED)
-            values (:1, :2, :3, :4, :5, :6, :7, :8)""",
+            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, AUTOSCHEDULEMODE, LOGINENABLED)
+            values (:1, :2, :3, :4, :5, :6, :7, :8, :9)""",
             (
                 record.uid,
                 "T" if record.enabled else "F",
@@ -637,6 +653,7 @@
                 "T" if record.enabledForCalendaring else "F",
                 "T" if record.enabledForAddressBooks else "F",
                 "T" if record.autoSchedule else "F",
+                record.autoScheduleMode if record.autoScheduleMode else "",
                 "T" if record.enabledForLogin else "F",
             )
         )
@@ -658,8 +675,8 @@
     def _addRecord(self, record):
         yield self.execute(
             """insert into AUGMENTS
-            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED)
-            values (:1, :2, :3, :4, :5, :6, :7, :8)""",
+            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, AUTOSCHEDULEMODE, LOGINENABLED)
+            values (:1, :2, :3, :4, :5, :6, :7, :8, :9)""",
             (
                 record.uid,
                 "T" if record.enabled else "F",
@@ -668,6 +685,7 @@
                 "T" if record.enabledForCalendaring else "F",
                 "T" if record.enabledForAddressBooks else "F",
                 "T" if record.autoSchedule else "F",
+                record.autoScheduleMode if record.autoScheduleMode else "",
                 "T" if record.enabledForLogin else "F",
             )
         )
@@ -676,8 +694,8 @@
     def _modifyRecord(self, record):
         yield self.execute(
             """update AUGMENTS set
-            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, LOGINENABLED) =
-            (:1, :2, :3, :4, :5, :6, :7 :8) where UID = :9""",
+            (UID, ENABLED, SERVERID, PARTITIONID, CALENDARING, ADDRESSBOOKS, AUTOSCHEDULE, AUTOSCHEDULEMODE, LOGINENABLED) =
+            (:1, :2, :3, :4, :5, :6, :7, :8, :9) where UID = :10""",
             (
                 record.uid,
                 "T" if record.enabled else "F",
@@ -686,6 +704,7 @@
                 "T" if record.enabledForCalendaring else "F",
                 "T" if record.enabledForAddressBooks else "F",
                 "T" if record.autoSchedule else "F",
+                record.autoScheduleMode if record.autoScheduleMode else "",
                 "T" if record.enabledForLogin else "F",
                 record.uid,
             )

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test -*-
 ##
-# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2012 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.
@@ -59,7 +59,6 @@
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
 from twisted.application import service
 from twisted.plugin import IPlugin
-from zope.interface import implements
 from xml.parsers.expat import ExpatError
 from plistlib import readPlistFromString
 
@@ -928,7 +927,9 @@
         self, service, recordType, guid=None,
         shortNames=(), authIDs=set(), fullName=None,
         firstName=None, lastName=None, emailAddresses=set(),
-        calendarUserAddresses=set(), autoSchedule=False, enabledForCalendaring=None,
+        calendarUserAddresses=set(),
+        autoSchedule=False, autoScheduleMode=None,
+        enabledForCalendaring=None,
         enabledForAddressBooks=None,
         uid=None,
         enabledForLogin=True,
@@ -962,6 +963,7 @@
         self.emailAddresses         = emailAddresses
         self.enabledForCalendaring  = enabledForCalendaring
         self.autoSchedule           = autoSchedule
+        self.autoScheduleMode       = autoScheduleMode
         self.enabledForAddressBooks = enabledForAddressBooks
         self.enabledForLogin        = enabledForLogin
         self.extProxies             = extProxies
@@ -1017,6 +1019,7 @@
             self.enabledForCalendaring = augment.enabledForCalendaring
             self.enabledForAddressBooks = augment.enabledForAddressBooks
             self.autoSchedule = augment.autoSchedule
+            self.autoScheduleMode = augment.autoScheduleMode
             self.enabledForLogin = augment.enabledForLogin
 
             if (self.enabledForCalendaring or self.enabledForAddressBooks) and self.recordType == self.service.recordType_groups:

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -52,23 +52,21 @@
 except ImportError:
     NegotiateCredentials = None
 from twisted.python.modules import getModule
+
+from twistedcaldav import caldavxml, customxml
+from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
 from twistedcaldav.config import config
-from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
-
-from twistedcaldav.extensions import DirectoryElement
-
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.directory.augment import allowedAutoScheduleModes
 from twistedcaldav.directory.common import uidsResourceName
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.idirectory import IDirectoryService
+from twistedcaldav.directory.wiki import getWikiACL
+from twistedcaldav.extensions import DirectoryElement
 from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVPrincipalResource,\
     DAVResourceWithChildrenMixin
-from twistedcaldav.resource import (
-    CalendarPrincipalCollectionResource, CalendarPrincipalResource
-)
-from twistedcaldav.directory.idirectory import IDirectoryService
-from twistedcaldav import caldavxml, customxml
-from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav.directory.wiki import getWikiACL
 
 thisModule = getModule(__name__)
 log = Logger()
@@ -964,6 +962,27 @@
     def getAutoSchedule(self):
         return self.record.autoSchedule
 
+    def canAutoSchedule(self):
+        """
+        Determine the auto-schedule state based on record state, type and config settings.
+        """
+        
+        if config.Scheduling.Options.AutoSchedule.Enabled:
+            if config.Scheduling.Options.AutoSchedule.Always or self.getAutoSchedule():
+                if self.getCUType() != "INDIVIDUAL" or config.Scheduling.Options.AutoSchedule.AllowUsers:
+                    return True
+        return False
+
+    @inlineCallbacks
+    def setAutoScheduleMode(self, autoScheduleMode):
+        self.record.autoScheduleMode = autoScheduleMode if autoScheduleMode in allowedAutoScheduleModes else "default"
+        augmentRecord = (yield self.record.service.augmentService.getAugmentRecord(self.record.guid, self.record.recordType))
+        augmentRecord.autoScheduleMode = autoScheduleMode
+        (yield self.record.service.augmentService.addAugmentRecords([augmentRecord]))
+
+    def getAutoScheduleMode(self):
+        return self.record.autoScheduleMode
+
     def getCUType(self):
         return self.record.getCUType()
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+Copyright (c) 2009-2012 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.
@@ -77,6 +77,7 @@
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
     <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>accept-always</auto-schedule-mode>
   </record>
   <record>
     <uid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</uid>
@@ -117,6 +118,24 @@
     <auto-schedule>true</auto-schedule>
   </record>
   <record>
+    <uid>C5BAADEE-6B35-4FD5-A98A-5DF6BBAAC47A</uid>
+    <enable>true</enable>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode/>
+  </record>
+  <record>
+    <uid>8AB34DF9-0297-4BA3-AADB-DB557DDD21E7</uid>
+    <enable>true</enable>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>accept-always</auto-schedule-mode>
+  </record>
+  <record>
     <uid>FC674703-8008-4A77-B80E-0DB55A9CE620</uid>
     <enable-login>false</enable-login>
   </record>

Modified: CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+Copyright (c) 2009-2012 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.
@@ -59,6 +59,24 @@
     <auto-schedule>true</auto-schedule>
   </record>
   <record>
+    <uid>C5BAADEE-6B35-4FD5-A98A-5DF6BBAAC47A</uid>
+    <enable>true</enable>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode/>
+  </record>
+  <record>
+    <uid>8AB34DF9-0297-4BA3-AADB-DB557DDD21E7</uid>
+    <enable>true</enable>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>accept-always</auto-schedule-mode>
+  </record>
+  <record>
     <uid>FC674703-8008-4A77-B80E-0DB55A9CE620</uid>
     <enable-login>false</enable-login>
   </record>

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2012 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.
@@ -28,44 +28,46 @@
 xmlFileDefault = os.path.join(os.path.dirname(__file__), "augments-test-default.xml")
 
 testRecords = (
-    {"uid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "enabled":True,  "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"6423F94A-6B76-4A3A-815B-D52CFD77935D", "enabled":True,  "partitionID":"", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":False},
-    {"uid":"5A985493-EE2C-4665-94CF-4DFEA3A89500", "enabled":False, "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "enabled":True,  "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"543D28BA-F74F-4D5F-9243-B3E3A61171E5", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"6A73326A-F781-47E7-A9F8-AF47364D4152", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":True},
+    {"uid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "enabled":True,  "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"6423F94A-6B76-4A3A-815B-D52CFD77935D", "enabled":True,  "partitionID":"", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"5A985493-EE2C-4665-94CF-4DFEA3A89500", "enabled":False, "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "enabled":True,  "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"543D28BA-F74F-4D5F-9243-B3E3A61171E5", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"6A73326A-F781-47E7-A9F8-AF47364D4152", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":True, "autoScheduleMode":"default"},
+    {"uid":"C5BAADEE-6B35-4FD5-A98A-5DF6BBAAC47A", "enabled":True,  "partitionID":"", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":True, "autoScheduleMode":"default"},
+    {"uid":"8AB34DF9-0297-4BA3-AADB-DB557DDD21E7", "enabled":True,  "partitionID":"", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":True, "autoScheduleMode":"accept-always"},
     {"uid":"FC674703-8008-4A77-B80E-0DB55A9CE620", "enabledForLogin":False,}, # Explicitly false
     {"uid":"B473DC32-1B0D-45EE-9BAC-DA878AE9CE74", "enabledForLogin":True,}, # Explicitly True
     {"uid":"9F2B176D-B3F5-483A-AA63-0A1FC6E6D54B", "enabledForLogin":True,}, # Default is True
 )
 
 testRecordWildcardDefault = (
-    {"uid":"A4318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":False},
-    {"uid":"AA5F935F-3358-4510-A649-B391D63279F2", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"ABF1A83B-1A29-4E04-BDC3-A6A66ECF27CA", "enabled":False, "partitionID":"",      "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"BC22A734-5E41-4FB7-B5C1-51DC0656DC2F", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":False},
-    {"uid":"C6DEEBB1-E14A-47F2-98BA-7E3BB4353E3A", "enabled":True,  "partitionID":"00003", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":True },
-    {"uid":"AA859321-2C72-4974-ADCF-0CBA0C76F95D", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"AB7C488B-9ED2-4265-881C-7E2E38A63584", "enabled":False, "partitionID":"",      "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
-    {"uid":"BB0C0DA1-0545-45F6-8D08-917C554D93A4", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":False},
-    {"uid":"CCD30AD3-582F-4682-8B65-2EDE92C5656E", "enabled":True,  "partitionID":"00003", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":True },
+    {"uid":"A4318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"AA5F935F-3358-4510-A649-B391D63279F2", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"ABF1A83B-1A29-4E04-BDC3-A6A66ECF27CA", "enabled":False, "partitionID":"",      "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"BC22A734-5E41-4FB7-B5C1-51DC0656DC2F", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"C6DEEBB1-E14A-47F2-98BA-7E3BB4353E3A", "enabled":True,  "partitionID":"00003", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":True,  "autoScheduleMode":"accept-always"},
+    {"uid":"AA859321-2C72-4974-ADCF-0CBA0C76F95D", "enabled":True,  "partitionID":"00001", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"AB7C488B-9ED2-4265-881C-7E2E38A63584", "enabled":False, "partitionID":"",      "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"BB0C0DA1-0545-45F6-8D08-917C554D93A4", "enabled":True,  "partitionID":"00002", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":False, "autoScheduleMode":"default"},
+    {"uid":"CCD30AD3-582F-4682-8B65-2EDE92C5656E", "enabled":True,  "partitionID":"00003", "enabledForCalendaring":True,  "enabledForAddressBooks":True,  "autoSchedule":True,  "autoScheduleMode":"accept-always"},
 )
 
 testRecordTypeDefault = (
-    ("locations", {"uid":"A4318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "partitionID":"00004", "enabledForCalendaring":True,  "enabledForAddressBooks":False,  "autoSchedule":True}),
-    ("locations", {"uid":"AA5F935F-3358-4510-A649-B391D63279F2", "enabled":True,  "partitionID":"00005", "enabledForCalendaring":True, "enabledForAddressBooks":False, "autoSchedule":True}),
-    ("resources", {"uid":"A5318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "partitionID":"00006", "enabledForCalendaring":True,  "enabledForAddressBooks":False,  "autoSchedule":True}),
-    ("resources", {"uid":"AA6F935F-3358-4510-A649-B391D63279F2", "enabled":True,  "partitionID":"00007", "enabledForCalendaring":True, "enabledForAddressBooks":False, "autoSchedule":True}),
+    ("locations", {"uid":"A4318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "partitionID":"00004", "enabledForCalendaring":True,  "enabledForAddressBooks":False,  "autoSchedule":True, "autoScheduleMode":"default"}),
+    ("locations", {"uid":"AA5F935F-3358-4510-A649-B391D63279F2", "enabled":True,  "partitionID":"00005", "enabledForCalendaring":True, "enabledForAddressBooks":False, "autoSchedule":True, "autoScheduleMode":"default"}),
+    ("resources", {"uid":"A5318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "partitionID":"00006", "enabledForCalendaring":True,  "enabledForAddressBooks":False,  "autoSchedule":True, "autoScheduleMode":"default"}),
+    ("resources", {"uid":"AA6F935F-3358-4510-A649-B391D63279F2", "enabled":True,  "partitionID":"00007", "enabledForCalendaring":True, "enabledForAddressBooks":False, "autoSchedule":True, "autoScheduleMode":"default"}),
 )
 
 
 testAddRecords = (
-    {"uid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975767", "enabled":True,  "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False},
+    {"uid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975767", "enabled":True,  "partitionID":"", "enabledForCalendaring":False, "enabledForAddressBooks":False, "autoSchedule":False, "autoScheduleMode":"default"},
 )
 
 testModifyRecords = (
-    {"uid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975767", "enabled":True,  "partitionID":"", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":False},
+    {"uid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975767", "enabled":True,  "partitionID":"", "enabledForCalendaring":True, "enabledForAddressBooks":True, "autoSchedule":False, "autoScheduleMode":"default"},
 )
 
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2012 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.
@@ -556,6 +556,40 @@
                 self.failIf(inboxURL)
                 self.failIf(outboxURL)
 
+    def test_canAutoSchedule(self):
+        """
+        DirectoryPrincipalResource.canAutoSchedule()
+        """
+        
+        # Set all resources and locations to auto-schedule, plus one user
+        for provisioningResource, recordType, recordResource, record in self._allRecords():
+            if record.enabledForCalendaring:
+                if recordType in ("locations", "resources") or record.uid == "cdaboo":
+                    recordResource.record.autoSchedule = True
+
+        # Default state - resources and locations, enabled, others not
+        for provisioningResource, recordType, recordResource, record in self._allRecords():
+            if record.enabledForCalendaring:
+                if recordType in ("locations", "resources"):
+                    self.assertTrue(recordResource.canAutoSchedule())
+                else:
+                    self.assertFalse(recordResource.canAutoSchedule())
+
+        # Set config to allow users
+        self.patch(config.Scheduling.Options.AutoSchedule, "AllowUsers", True)
+        for provisioningResource, recordType, recordResource, record in self._allRecords():
+            if record.enabledForCalendaring:
+                if recordType in ("locations", "resources") or record.uid == "cdaboo":
+                    self.assertTrue(recordResource.canAutoSchedule())
+                else:
+                    self.assertFalse(recordResource.canAutoSchedule())
+
+        # Set config to disallow all
+        self.patch(config.Scheduling.Options.AutoSchedule, "Enabled", False)
+        for provisioningResource, recordType, recordResource, record in self._allRecords():
+            if record.enabledForCalendaring:
+                self.assertFalse(recordResource.canAutoSchedule())
+
     @inlineCallbacks
     def test_defaultAccessControlList_principals(self):
         """

Modified: CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2012 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.
@@ -42,6 +42,7 @@
 ELEMENT_ENABLEADDRESSBOOK = "enable-addressbook"
 ELEMENT_ENABLELOGIN       = "enable-login"
 ELEMENT_AUTOSCHEDULE      = "auto-schedule"
+ELEMENT_AUTOSCHEDULE_MODE = "auto-schedule-mode"
 
 ATTRIBUTE_REPEAT          = "repeat"
 
@@ -58,6 +59,7 @@
     ELEMENT_ENABLEADDRESSBOOK: "enabledForAddressBooks",
     ELEMENT_ENABLELOGIN:       "enabledForLogin",
     ELEMENT_AUTOSCHEDULE:      "autoSchedule",
+    ELEMENT_AUTOSCHEDULE_MODE: "autoScheduleMode",
 }
 
 class XMLAugmentsParser(object):
@@ -100,6 +102,7 @@
                     ELEMENT_SERVERID,
                     ELEMENT_PARTITIONID,
                     ELEMENT_HOSTEDAT,
+                    ELEMENT_AUTOSCHEDULE_MODE,
                 ):
                     fields[node.tag] = node.text if node.text else ""
                 elif node.tag in (

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.test.test_resource,twistedcaldav.test.test_wrapping -*-
 ##
-# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2012 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.
@@ -1872,6 +1872,7 @@
                 (calendarserver_namespace, "calendar-proxy-read-for"  ),
                 (calendarserver_namespace, "calendar-proxy-write-for" ),
                 (calendarserver_namespace, "auto-schedule" ),
+                (calendarserver_namespace, "auto-schedule-mode" ),
             )
         
         if self.addressBooksEnabled():
@@ -1967,6 +1968,10 @@
                 autoSchedule = self.getAutoSchedule()
                 returnValue(customxml.AutoSchedule("true" if autoSchedule else "false"))
 
+            elif name == "auto-schedule-mode" and self.calendarsEnabled():
+                autoScheduleMode = self.getAutoScheduleMode()
+                returnValue(customxml.AutoScheduleMode(autoScheduleMode if autoScheduleMode else "default"))
+
         elif namespace == carddav_namespace and self.addressBooksEnabled():
             if name == "addressbook-home-set":
                 returnValue(carddavxml.AddressBookHomeSet(

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2011 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2012 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.
@@ -50,7 +50,7 @@
 class iTipProcessing(object):
 
     @staticmethod
-    def processNewRequest(itip_message, recipient=None, autoprocessing=False):
+    def processNewRequest(itip_message, recipient=None):
         """
         Process a METHOD=REQUEST for a brand new calendar object.
         
@@ -65,14 +65,14 @@
         method = calendar.getProperty("METHOD")
         if method:
             calendar.removeProperty(method)
-        
-        if recipient and not autoprocessing:
-            iTipProcessing.fixForiCal3(calendar.subcomponents(), recipient, config.Scheduling.CalDAV.OldDraftCompatibility)
 
+        if recipient:
+            iTipProcessing.addTranspForNeedsAction(calendar.subcomponents(), recipient)
+
         return calendar
         
     @staticmethod
-    def processRequest(itip_message, calendar, recipient, autoprocessing=False):
+    def processRequest(itip_message, calendar, recipient):
         """
         Process a METHOD=REQUEST. We need to merge per-attendee properties such as TRANPS, COMPLETED etc
         with the data coming from the organizer.
@@ -117,7 +117,7 @@
         if itip_message.masterComponent() is not None:
             
             # Get a new calendar object first
-            new_calendar = iTipProcessing.processNewRequest(itip_message, recipient, autoprocessing)
+            new_calendar = iTipProcessing.processNewRequest(itip_message, recipient)
             
             # Copy over master alarms, comments
             master_component = new_calendar.masterComponent()
@@ -167,8 +167,8 @@
                     component = component.duplicate()
                     iTipProcessing.transferItems(calendar, master_valarms, private_comments, transps, completeds, organizer_schedule_status, component, remove_matched=True)
                     calendar.addComponent(component)
-                    if recipient and not autoprocessing:
-                        iTipProcessing.fixForiCal3((component,), recipient, config.Scheduling.CalDAV.OldDraftCompatibility)
+                    if recipient:
+                        iTipProcessing.addTranspForNeedsAction((component,), recipient)
 
             # Write back the modified object
             return calendar, rids
@@ -507,21 +507,15 @@
                     organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
     
     @staticmethod
-    def fixForiCal3(components, recipient, compatibilityMode):
+    def addTranspForNeedsAction(components, recipient):
         # For each component where the ATTENDEE property of the recipient has PARTSTAT
-        # NEEDS-ACTION we need to add X-APPLE-NEEDS-REPLY:TRUE
-        # We also add TRANSP:TRANSPARENT for VEVENTs
+        # NEEDS-ACTION we add TRANSP:TRANSPARENT for VEVENTs
         for component in components:
-            if component.name() == "VTIMEZONE":
+            if component.name() != "VEVENT":
                 continue
             attendee = component.getAttendeeProperty((recipient,))
-            if attendee:
-                partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
-                if partstat == "NEEDS-ACTION":
-                    if compatibilityMode:
-                        component.addProperty(Property("X-APPLE-NEEDS-REPLY", "TRUE"))
-                    if component.name() == "VEVENT":
-                        component.replaceProperty(Property("TRANSP", "TRANSPARENT"))
+            if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "NEEDS-ACTION":
+                component.replaceProperty(Property("TRANSP", "TRANSPARENT"))
 
     @staticmethod
     def sequenceComparison(itip, calendar):

Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -454,18 +454,22 @@
                 raise ImplicitProcessorException(iTIPRequestStatus.NO_USER_SUPPORT)
 
             log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
-            autoprocessed = self.recipient.principal.getAutoSchedule()
-            store_inbox = not autoprocessed or self.recipient.principal.getCUType() == "INDIVIDUAL"
-            new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr, autoprocessing=autoprocessed)
+            new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr)
             name =  md5(str(new_calendar) + str(time.time()) + default.url()).hexdigest() + ".ics"
             
             # Handle auto-reply behavior
-            if autoprocessed:
-                send_reply, partstat = (yield self.checkAttendeeAutoReply(new_calendar))
+            if self.recipient.principal.canAutoSchedule():
+                send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, self.recipient.principal.getAutoScheduleMode()))
+                
+                # Only store inbox item when reply is not sent or always for users
+                store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
+            else:
+                send_reply = False
+                store_inbox = True
 
             new_resource = (yield self.writeCalendarResource(default.url(), default, name, new_calendar))
             
-            if autoprocessed and send_reply:
+            if send_reply:
                 # Track outstanding auto-reply processing
                 if not hasattr(self.request, "auto_reply_processing_count"):
                     self.request.auto_reply_processing_count = 1
@@ -480,23 +484,27 @@
                     customxml.Create(),
                 ),
             )
-            result = (True, autoprocessed, store_inbox, changes,)
+            result = (True, send_reply, store_inbox, changes,)
         else:
             # Processing update to existing event
-            autoprocessed = self.recipient.principal.getAutoSchedule()
-            store_inbox = not autoprocessed or self.recipient.principal.getCUType() == "INDIVIDUAL"
-            new_calendar, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr, autoprocessing=autoprocessed)
+            new_calendar, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
             if new_calendar:
      
                 # Handle auto-reply behavior
-                if autoprocessed:
-                    send_reply, partstat = (yield self.checkAttendeeAutoReply(new_calendar))
+                if self.recipient.principal.canAutoSchedule():
+                    send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, self.recipient.principal.getAutoScheduleMode()))
+                    
+                    # Only store inbox item when reply is not sent or always for users
+                    store_inbox = store_inbox or self.recipient.principal.getCUType() == "INDIVIDUAL"
+                else:
+                    send_reply = False
+                    store_inbox = True
 
                 # Update the attendee's copy of the event
                 log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
                 new_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, new_calendar))
                 
-                if autoprocessed and send_reply:
+                if send_reply:
                     # Track outstanding auto-reply processing
                     if not hasattr(self.request, "auto_reply_processing_count"):
                         self.request.auto_reply_processing_count = 1
@@ -530,7 +538,7 @@
                 if hasattr(self.request, "doing_attendee_refresh"):
                     store_inbox = False
 
-                result = (True, autoprocessed, store_inbox, changes,)
+                result = (True, send_reply, store_inbox, changes,)
                 
             else:
                 # Request needs to be ignored
@@ -654,7 +662,7 @@
                 self.request.auto_reply_processing_count -= 1
 
     @inlineCallbacks
-    def checkAttendeeAutoReply(self, calendar):
+    def checkAttendeeAutoReply(self, calendar, automode):
         """
         Check whether a reply to the given iTIP message is needed. We will not process a reply
         A reply will either be positive (accepted invitation) or negative (denied invitation).
@@ -664,94 +672,120 @@
         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{bool} indicating whether changes were made.
+        @param calendar: the iTIP message to process
+        @type calendar: L{Component}
+        @param automode: the auto-schedule mode for the recipient
+        @type automode: C{str}
+
+        @return: C{tuple} of C{bool}, C{bool}, C{str} indicating whether changes were made, whether the inbox item
+            should be added, and the new PARTSTAT.
         """
         
-        log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply" % (self.recipient.cuaddr, self.uid))
+        # First ignore the none mode
+        if automode == "none":
+            returnValue((False, True, "",))
+        elif not automode or automode == "default":
+            automode = config.Scheduling.Options.AutoSchedule.DefaultMode
 
-        # First expand current one to get instances (only go 1 year into the future)
-        default_future_expansion_duration = PyCalendarDuration(days=356*1)
-        expand_max = PyCalendarDateTime.getToday() + default_future_expansion_duration
-        instances = calendar.expandTimeRanges(expand_max, ignoreInvalidInstances=True)
-        instance_states = dict([(instance, True) for instance in instances.instances.itervalues()])
+        log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s" % (self.recipient.cuaddr, self.uid, automode,))
+
+        # The accept-always and decline-always modes do not need any freebusy checks
+        if automode in ("accept-always", "decline-always",):
+            all_accepted = automode == "accept-always"
+            all_declined = automode == "decline-always"
         
-        # Extract UID from primary component as we want to ignore this one if we match it
-        # in any calendars.
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid = comp.propertyValue("UID")
+        # Other modes need freebusy check
+        else:
+            # First expand current one to get instances (only go 1 year into the future)
+            default_future_expansion_duration = PyCalendarDuration(days=356*1)
+            expand_max = PyCalendarDateTime.getToday() + default_future_expansion_duration
+            instances = calendar.expandTimeRanges(expand_max, ignoreInvalidInstances=True)
+            instance_states = dict([(instance, True) for instance in instances.instances.itervalues()])
+            
+            # Extract UID from primary component as we want to ignore this one if we match it
+            # in any calendars.
+            comp = calendar.mainComponent(allow_multiple=True)
+            uid = comp.propertyValue("UID")
+        
+            # Now compare each instance time-range with the index and see if there is an overlap
+            calendars = (yield self._getCalendarsToMatch())
+        
+            for calURL in calendars:
+                testcal = (yield self.request.locateResource(calURL))
     
-        # Now compare each instance time-range with the index and see if there is an overlap
-        calendars = (yield self._getCalendarsToMatch())
+                # Get the timezone property from the collection, and store in the query filter
+                # for use during the query itself.
+                has_prop = (yield testcal.hasProperty((caldav_namespace, "calendar-timezone"), self.request))
+                if has_prop:
+                    tz = (yield testcal.readProperty((caldav_namespace, "calendar-timezone"), self.request))
+                    tzinfo = tz.calendar().gettimezone()
+                else:
+                    tzinfo = PyCalendarTimezone(utc=True)
     
-        for calURL in calendars:
-            testcal = (yield self.request.locateResource(calURL))
-
-            # Get the timezone property from the collection, and store in the query filter
-            # for use during the query itself.
-            has_prop = (yield testcal.hasProperty((caldav_namespace, "calendar-timezone"), self.request))
-            if has_prop:
-                tz = (yield testcal.readProperty((caldav_namespace, "calendar-timezone"), self.request))
-                tzinfo = tz.calendar().gettimezone()
-            else:
-                tzinfo = PyCalendarTimezone(utc=True)
-
-            # Now do search for overlapping time-range
-            for instance in instances.instances.itervalues():
-                if instance_states[instance]:
-                    try:
-                        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-                        fbinfo = ([], [], [])
-                        
-                        def makeTimedUTC(dt):
-                            dt = dt.duplicate()
-                            if dt.isDateOnly():
-                                dt.setDateOnly(False)
-                                dt.setHHMMSS(0, 0, 0)
-                            if dt.floating():
-                                dt.setTimezone(tzinfo)
-                                dt.adjustToUTC()
-                            return dt
-                        
-                        tr = caldavxml.TimeRange(
-                            start=str(makeTimedUTC(instance.start)),
-                            end=str(makeTimedUTC(instance.end)),
-                        )
-
-                        yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
-                        
-                        # If any fbinfo entries exist we have an overlap
-                        if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+                # Now do search for overlapping time-range
+                for instance in instances.instances.itervalues():
+                    if instance_states[instance]:
+                        try:
+                            # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+                            fbinfo = ([], [], [])
+                            
+                            def makeTimedUTC(dt):
+                                dt = dt.duplicate()
+                                if dt.isDateOnly():
+                                    dt.setDateOnly(False)
+                                    dt.setHHMMSS(0, 0, 0)
+                                if dt.floating():
+                                    dt.setTimezone(tzinfo)
+                                    dt.adjustToUTC()
+                                return dt
+                            
+                            tr = caldavxml.TimeRange(
+                                start=str(makeTimedUTC(instance.start)),
+                                end=str(makeTimedUTC(instance.end)),
+                            )
+    
+                            yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
+                            
+                            # If any fbinfo entries exist we have an overlap
+                            if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+                                instance_states[instance] = False
+                        except NumberOfMatchesWithinLimits:
                             instance_states[instance] = False
-                    except NumberOfMatchesWithinLimits:
-                        instance_states[instance] = False
-                        log.info("Exceeded number of matches whilst trying to find free-time.")
+                            log.info("Exceeded number of matches whilst trying to find free-time.")
+                
+                # If everything is declined we can exit now
+                if not any(instance_states.itervalues()):
+                    break
             
-            # If everything is declined we can exit now
-            if not any(instance_states.itervalues()):
-                break
-        
-        # TODO: here we should do per-instance ACCEPT/DECLINE behavior
-        # For now we will assume overall ACCEPT/DECLINE
+            # TODO: here we should do per-instance ACCEPT/DECLINE behavior
+            # For now we will assume overall ACCEPT/DECLINE
+    
+            # Collect all the accepted and declined states
+            all_accepted = all(instance_states.itervalues())
+            all_declined = not any(instance_states.itervalues())
 
-        # Collect all the accepted and declined states
-        all_accepted = all(instance_states.itervalues())
-        all_declined = not any(instance_states.itervalues())
-
         # Do the simple case of all accepted or decline separately
         cuas = self.recipient.principal.calendarUserAddresses()
         if all_accepted or all_declined:
             # Extract the ATTENDEE property matching current recipient from the calendar data
             attendeeProps = calendar.getAttendeeProperties(cuas)
             if not attendeeProps:
-                returnValue((False, "",))
+                returnValue((False, True, "",))
         
-            if all_accepted:
-                partstat = "ACCEPTED"
+            if automode == "accept-always":
+                freePartstat = busyPartstat = "ACCEPTED"
+            elif automode == "decline-always":
+                freePartstat = busyPartstat = "DECLINED"
             else:
-                partstat = "DECLINED"
-            calendar.replacePropertyInAllComponents(Property("TRANSP", "OPAQUE" if all_accepted else "TRANSPARENT"))
+                freePartstat = "ACCEPTED" if automode in ("accept-if-free", "automatic",) else "NEEDS-ACTION"
+                busyPartstat = "DECLINED" if automode in ("decline-if-busy", "automatic",) else "NEEDS-ACTION"
+            freeStateOpaque = freePartstat == "ACCEPTED"
+
+            partstat = freePartstat if all_accepted else busyPartstat
+            calendar.replacePropertyInAllComponents(Property("TRANSP", "OPAQUE" if all_accepted and freeStateOpaque else "TRANSPARENT"))
     
             made_changes = self.changeAttendeePartstat(attendeeProps, partstat)
+            store_inbox = partstat == "NEEDS-ACTION"
         
         else:
             # Hard case: some accepted some declined
@@ -760,24 +794,34 @@
             # any other declines.
             
             made_changes = False
+            store_inbox = False
             partstat = "MIXED RESPONSE"
+            
+            freePartstat = "ACCEPTED" if automode in ("accept-if-free", "automatic",) else "NEEDS-ACTION"
+            busyPartstat = "DECLINED" if automode in ("decline-if-busy", "automatic",) else "NEEDS-ACTION"
+            freeStateOpaque = freePartstat == "ACCEPTED"
 
-            # Default state is whichever of ACCEPTED or DECLINED has most instances
-            defaultStateAccepted = len(filter(lambda x:x, instance_states.values())) >= len(instance_states.keys()) / 2
+            # Default state is whichever of free or busy has most instances
+            defaultStateFree = len(filter(lambda x:x, instance_states.values())) >= len(instance_states.keys()) / 2
 
             # See if there is a master component first
+            hadMasterRsvp = False
             master = calendar.masterComponent()
             if master:
                 attendee = master.getAttendeeProperty(cuas)
                 if attendee:
-                    made_changes |= self.changeAttendeePartstat(attendee, "ACCEPTED" if defaultStateAccepted else "DECLINED")
-                    master.replaceProperty(Property("TRANSP", "OPAQUE" if defaultStateAccepted else "TRANSPARENT"))
+                    hadMasterRsvp = attendee.parameterValue("RSVP", "FALSE") == "TRUE"
+                    new_partstat = freePartstat if defaultStateFree else busyPartstat
+                    if new_partstat == "NEEDS-ACTION":
+                        store_inbox = True
+                    made_changes |= self.changeAttendeePartstat(attendee, new_partstat)
+                    master.replaceProperty(Property("TRANSP", "OPAQUE" if defaultStateFree and freeStateOpaque else "TRANSPARENT"))
 
             # Look at expanded instances and change partstat accordingly
-            for instance, accepted in sorted(instance_states.iteritems(), key=lambda x: x[0].rid):
+            for instance, free in sorted(instance_states.iteritems(), key=lambda x: x[0].rid):
                 
                 overridden = calendar.overriddenComponent(instance.rid)
-                if not overridden and accepted == defaultStateAccepted:
+                if not overridden and free == defaultStateFree:
                     # Nothing to do as state matches the master
                     continue 
                 
@@ -785,16 +829,23 @@
                     # Change ATTENDEE property to match new state
                     attendee = overridden.getAttendeeProperty(cuas)
                     if attendee:
-                        made_changes |= self.changeAttendeePartstat(attendee, "ACCEPTED" if accepted else "DECLINED")
-                        overridden.replaceProperty(Property("TRANSP", "OPAQUE" if accepted else "TRANSPARENT"))
+                        new_partstat = freePartstat if free else busyPartstat
+                        if new_partstat == "NEEDS-ACTION":
+                            store_inbox = True
+                        made_changes |= self.changeAttendeePartstat(attendee, new_partstat)
+                        overridden.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
                 else:
-                    # Derive a new overridden component and change partstat
+                    # Derive a new overridden component and change partstat. We also need to make sure we restore any RSVP
+                    # value that may have been overwritten by any change to the master itself. 
                     derived = calendar.deriveInstance(instance.rid)
                     if derived:
                         attendee = derived.getAttendeeProperty(cuas)
                         if attendee:
-                            self.changeAttendeePartstat(attendee, "ACCEPTED" if accepted else "DECLINED")
-                            derived.replaceProperty(Property("TRANSP", "OPAQUE" if accepted else "TRANSPARENT"))
+                            new_partstat = freePartstat if free else busyPartstat
+                            if new_partstat == "NEEDS-ACTION":
+                                store_inbox = True
+                            self.changeAttendeePartstat(attendee, new_partstat, hadMasterRsvp)
+                            derived.replaceProperty(Property("TRANSP", "OPAQUE" if free and freeStateOpaque else "TRANSPARENT"))
                             calendar.addComponent(derived)
                             made_changes = True
             
@@ -802,7 +853,7 @@
         if made_changes:
             calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.MESSAGE_DELIVERED_CODE, "ORGANIZER", None)
         
-        returnValue((made_changes, partstat,))
+        returnValue((made_changes, store_inbox, partstat,))
 
     def _getCalendarsToMatch(self):
         # Determine the set of calendar URIs for a principal need to be searched.
@@ -868,7 +919,7 @@
         yield delchild.storeRemove(self.request, False, childURL)
 
 
-    def changeAttendeePartstat(self, attendees, partstat):
+    def changeAttendeePartstat(self, attendees, partstat, hadRSVP=False):
         """
         Change the PARTSTAT on any ATTENDEE properties passed in.
 
@@ -876,6 +927,8 @@
         @type attendees: L{Property}, C{list} or C{tuple}
         @param partstat: new PARTSTAT to set
         @type partstat: C{str}
+        @param hadRSVP: indicates whether RSVP should be added when changing to NEEDS-ACTION
+        @type hadRSVP: C{bool}
         
         @return: C{True} if any change was made, C{False} otherwise
         """
@@ -889,10 +942,13 @@
                 attendee.setParameter("PARTSTAT", partstat)
                 madeChanges = True
 
-            # Always remove RSVP - this is only an attendee change so madeChanges
-            # does not need to be changed
+            # Always remove RSVP when a state other than NEEDS-ACTION is set - this
+            # is only an attendee change so madeChanges does not need to be changed
             try:
-                attendee.removeParameter("RSVP")
+                if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") != "NEEDS-ACTION":
+                    attendee.removeParameter("RSVP")
+                elif hadRSVP:
+                    attendee.setParameter("RSVP", "TRUE")
             except KeyError:
                 pass
 

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2012-02-09 21:23:20 UTC (rev 8641)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2012-02-09 21:34:19 UTC (rev 8642)
@@ -633,13 +633,25 @@
             "AllowGroupAsOrganizer"               : False, # Allow groups to be Organizers
             "AllowLocationAsOrganizer"            : False, # Allow locations to be Organizers
             "AllowResourceAsOrganizer"            : False, # Allow resources to be Organizers
-            "AllowUserAutoAccept"                 : False, # Allow auto-accept for users
             "LimitFreeBusyAttendees"              :  30,    # Maximum number of attendees to request freebusy for
             "AttendeeRefreshBatch"                :   5,    # Number of attendees to do batched refreshes: 0 - no batching
             "AttendeeRefreshBatchDelaySeconds"    :   5,    # Time after an iTIP REPLY for first batched attendee refresh
             "AttendeeRefreshBatchIntervalSeconds" :   5,    # Time between attendee batch refreshes
             "UIDLockTimeoutSeconds"               :  60,    # Time for implicit UID lock timeout
-            "UIDLockExpirySeconds"                : 300,    # Expiration time for UID lock
+            "UIDLockExpirySeconds"                : 300,    # Expiration time for UID lock,
+            
+            "AutoSchedule" : {
+                "Enabled"                         : True,   # Auto-scheduling will never occur if set to False
+                "Always"                          : False,  # Override augments setting and always auto-schedule
+                "AllowUsers"                      : False,  # Allow auto-schedule for users
+                "DefaultMode"                     : "automatic",   # Default mode for auto-schedule processing, one of:
+                                                                   # "none"            - no auto-scheduling
+                                                                   # "accept-always"   - always accept, ignore busy time
+                                                                   # "decline-always"  - always decline, ignore free time
+                                                                   # "accept-if-free"  - accept if free, do nothing if busy
+                                                                   # "decline-if-busy" - decline if busy, do nothing if free
+                                                                   # "automatic"       - accept if free, decline if busy
+            }
         }
     },
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120209/228a23eb/attachment-0001.html>


More information about the calendarserver-changes mailing list