[CalendarServer-changes] [11051] CalendarServer/branches/users/glyph/sharedgroups-2

source_changes at macosforge.org source_changes at macosforge.org
Tue Apr 16 15:19:02 PDT 2013


Revision: 11051
          http://trac.calendarserver.org//changeset/11051
Author:   glyph at apple.com
Date:     2013-04-16 15:19:02 -0700 (Tue, 16 Apr 2013)
Log Message:
-----------
Up to 10980.

Modified Paths:
--------------
    CalendarServer/branches/users/glyph/sharedgroups-2/calendarserver/tap/caldav.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twisted/plugins/caldav.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/test/test_directory.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/ical.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/scheduling/imip/inbound.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_icalendar.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_validation.py
    CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/upgrade.py
    CalendarServer/branches/users/glyph/sharedgroups-2/txdav/common/datastore/sql_schema/current.sql

Added Paths:
-----------
    CalendarServer/branches/users/glyph/sharedgroups-2/.gitignore

Property Changed:
----------------
    CalendarServer/branches/users/glyph/sharedgroups-2/


Property changes on: CalendarServer/branches/users/glyph/sharedgroups-2
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:9885-10970
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:9885-10980

Copied: CalendarServer/branches/users/glyph/sharedgroups-2/.gitignore (from rev 10980, CalendarServer/trunk/.gitignore)
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/.gitignore	                        (rev 0)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/.gitignore	2013-04-16 22:19:02 UTC (rev 11051)
@@ -0,0 +1,8 @@
+*.pyc
+*.so
+dropin.cache
+
+/build/
+/data/
+/calendarserver/version.py
+/conf/caldavd-dev.plist
\ No newline at end of file

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/calendarserver/tap/caldav.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/calendarserver/tap/caldav.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -70,6 +70,8 @@
 
 from twistedcaldav.config import ConfigurationError
 from twistedcaldav.config import config
+from twistedcaldav.directory import calendaruserproxy
+from twistedcaldav.directory.directory import GroupMembershipCacheUpdater
 from twistedcaldav.localization import processLocalizationFiles
 from twistedcaldav import memcachepool
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
@@ -542,29 +544,8 @@
             )
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
-        if config.GroupCaching.Enabled and config.GroupCaching.EnableUpdater:
-            self.maker.log_info("Adding group caching service")
 
-            groupMembershipCacherArgv = [
-                sys.executable,
-                sys.argv[0],
-            ]
-            if config.UserName:
-                groupMembershipCacherArgv.extend(("-u", config.UserName))
-            if config.GroupName:
-                groupMembershipCacherArgv.extend(("-g", config.GroupName))
-            groupMembershipCacherArgv.extend((
-                "--reactor=%s" % (config.Twisted.reactor,),
-                "-n", self.maker.groupMembershipCacherTapName,
-                "-f", self.configPath,
-                "-o", "PIDFile=groupcacher.pid",
-            ))
 
-            self.monitor.addProcess("groupcacher", groupMembershipCacherArgv,
-                               env=PARENT_ENVIRONMENT)
-
-
-
 class ReExecService(MultiService, LoggingMixIn):
     """
     A MultiService which catches SIGHUP and re-exec's the process.
@@ -621,12 +602,7 @@
     description = "Calendar and Contacts Server"
     options = CalDAVOptions
 
-    #
-    # Default tap names
-    #
-    groupMembershipCacherTapName = "caldav_groupcacher"
 
-
     def makeService(self, options):
         """
         Create the top-level service.
@@ -774,10 +750,24 @@
         else:
             mailRetriever = None
 
+        # Optionally set up group cacher
+        if config.GroupCaching.Enabled:
+            groupCacher = GroupMembershipCacheUpdater(
+                calendaruserproxy.ProxyDBService,
+                directory,
+                config.GroupCaching.UpdateSeconds,
+                config.GroupCaching.ExpireSeconds,
+                namespace=config.GroupCaching.MemcachedPool,
+                useExternalProxies=config.GroupCaching.UseExternalProxies
+                )
+        else:
+            groupCacher = None
+
         def decorateTransaction(txn):
             txn._pushDistributor = pushDistributor
             txn._rootResource = result.rootResource
             txn._mailRetriever = mailRetriever
+            txn._groupCacher = groupCacher
 
         store.callWithNewTransactions(decorateTransaction)
 
@@ -1016,11 +1006,35 @@
     def makeService_Single(self, options):
         """
         Create a service to be used in a single-process, stand-alone
-        configuration.
+        configuration.  Memcached will be spawned automatically.
         """
         def slaveSvcCreator(pool, store, logObserver):
             result = self.requestProcessingService(options, store, logObserver)
 
+            # Optionally launch memcached.  Note, this is not going through a
+            # ProcessMonitor because there is code elsewhere that needs to
+            # access memcached before startService() gets called, so we're just
+            # directly using Popen to spawn memcached.
+            for name, pool in config.Memcached.Pools.items():
+                if pool.ServerEnabled:
+                    self.log_info(
+                        "Adding memcached service for pool: %s" % (name,)
+                    )
+                    memcachedArgv = [
+                        config.Memcached.memcached,
+                        "-p", str(pool.Port),
+                        "-l", pool.BindAddress,
+                        "-U", "0",
+                    ]
+                    if config.Memcached.MaxMemory is not 0:
+                        memcachedArgv.extend(
+                            ["-m", str(config.Memcached.MaxMemory)]
+                        )
+                    if config.UserName:
+                        memcachedArgv.extend(["-u", config.UserName])
+                    memcachedArgv.extend(config.Memcached.Options)
+                    Popen(memcachedArgv)
+
             # Optionally set up push notifications
             pushDistributor = None
             if config.Notifications.Enabled:
@@ -1049,10 +1063,24 @@
             else:
                 mailRetriever = None
 
+            # Optionally set up group cacher
+            if config.GroupCaching.Enabled:
+                groupCacher = GroupMembershipCacheUpdater(
+                    calendaruserproxy.ProxyDBService,
+                    directory,
+                    config.GroupCaching.UpdateSeconds,
+                    config.GroupCaching.ExpireSeconds,
+                    namespace=config.GroupCaching.MemcachedPool,
+                    useExternalProxies=config.GroupCaching.UseExternalProxies
+                    )
+            else:
+                groupCacher = None
+
             def decorateTransaction(txn):
                 txn._pushDistributor = pushDistributor
                 txn._rootResource = result.rootResource
                 txn._mailRetriever = mailRetriever
+                txn._groupCacher = groupCacher
 
             store.callWithNewTransactions(decorateTransaction)
 

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twisted/plugins/caldav.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twisted/plugins/caldav.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twisted/plugins/caldav.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -53,4 +53,3 @@
 
 
 TwistedCalDAV = TAP("calendarserver.tap.caldav.CalDAVServiceMaker")
-CalDAVGroupCacher = TAP("twistedcaldav.directory.directory.GroupMembershipCacherServiceMaker")

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/directory.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/directory.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -26,6 +26,7 @@
     "DirectoryError",
     "DirectoryConfigurationError",
     "UnknownRecordTypeError",
+    "GroupMembershipCacheUpdater",
 ]
 
 import cPickle as pickle
@@ -34,7 +35,6 @@
 import itertools
 import os
 import pwd
-import signal
 import sys
 import types
 
@@ -46,24 +46,27 @@
 from twext.web2.dav.auth import IPrincipalCredentials
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 
-from twext.python.log import LoggingMixIn
+from twext.python.log import Logger, LoggingMixIn
 
 from twistedcaldav.config import config
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+
 from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
 from twistedcaldav.directory.util import uuidFromName, normalizeUUID
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 from twistedcaldav.scheduling.ischedule.localservers import Servers
 from twistedcaldav.memcacher import Memcacher
-from twistedcaldav import memcachepool
 from twisted.python.filepath import FilePath
-from twisted.python.reflect import namedClass
-from twisted.python.usage import Options, UsageError
-from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
-from twisted.application import service
-from twisted.plugin import IPlugin
 from xml.parsers.expat import ExpatError
 from plistlib import readPlistFromString
+from twext.enterprise.dal.record import fromTable
+from twext.enterprise.queue import WorkItem
+from txdav.common.datastore.sql_tables import schema
+from twext.enterprise.dal.syntax import Delete
 
+log = Logger()
+
+
 class DirectoryService(LoggingMixIn):
     implements(IDirectoryService, ICredentialsChecker)
 
@@ -564,20 +567,16 @@
 
     "group-cacher-populated" : contains a datestamp indicating the most recent
     population.
-
-    "group-cacher-lock" : used to prevent multiple updates, it has a value of "1"
-
     """
 
     def __init__(self, namespace, pickle=True, no_invalidation=False,
-        key_normalization=True, expireSeconds=0, lockSeconds=60):
+        key_normalization=True, expireSeconds=0):
 
         super(GroupMembershipCache, self).__init__(namespace, pickle=pickle,
             no_invalidation=no_invalidation,
             key_normalization=key_normalization)
 
         self.expireSeconds = expireSeconds
-        self.lockSeconds = lockSeconds
 
 
     def setGroupsFor(self, guid, memberships):
@@ -616,17 +615,7 @@
         returnValue(value is not None)
 
 
-    def acquireLock(self):
-        self.log_debug("add group-cacher-lock")
-        return self.add("group-cacher-lock", "1", expireTime=self.lockSeconds)
 
-
-    def releaseLock(self):
-        self.log_debug("delete group-cacher-lock")
-        return self.delete("group-cacher-lock")
-
-
-
 class GroupMembershipCacheUpdater(LoggingMixIn):
     """
     Responsible for updating memcached with group memberships.  This will run
@@ -634,11 +623,12 @@
     proxy database, and the location/resource info in the directory system.
     """
 
-    def __init__(self, proxyDB, directory, expireSeconds, lockSeconds,
+    def __init__(self, proxyDB, directory, updateSeconds, expireSeconds,
         cache=None, namespace=None, useExternalProxies=False,
         externalProxiesSource=None):
         self.proxyDB = proxyDB
         self.directory = directory
+        self.updateSeconds = updateSeconds
         self.useExternalProxies = useExternalProxies
         if useExternalProxies and externalProxiesSource is None:
             externalProxiesSource = self.directory.getExternalProxyAssignments
@@ -646,8 +636,7 @@
 
         if cache is None:
             assert namespace is not None, "namespace must be specified if GroupMembershipCache is not provided"
-            cache = GroupMembershipCache(namespace, expireSeconds=expireSeconds,
-                lockSeconds=lockSeconds)
+            cache = GroupMembershipCache(namespace, expireSeconds=expireSeconds)
         self.cache = cache
 
 
@@ -738,8 +727,6 @@
         # See if anyone has completely populated the group membership cache
         isPopulated = (yield self.cache.isPopulated())
 
-        useLock = True
-
         if fast:
             # We're in quick-start mode.  Check first to see if someone has
             # populated the membership cache, and if so, return immediately
@@ -747,9 +734,6 @@
                 self.log_info("Group membership cache is already populated")
                 returnValue((fast, 0))
 
-            # We don't care what others are doing right now, we need to update
-            useLock = False
-
         self.log_info("Updating group membership cache")
 
         dataRoot = FilePath(config.DataRoot)
@@ -767,14 +751,6 @@
             previousMembers = pickle.loads(membershipsCacheFile.getContent())
             callGroupsChanged = True
 
-        if useLock:
-            self.log_info("Attempting to acquire group membership cache lock")
-            acquiredLock = (yield self.cache.acquireLock())
-            if not acquiredLock:
-                self.log_info("Group membership cache lock held by another process")
-                returnValue((fast, 0))
-            self.log_info("Acquired lock")
-
         if not fast and self.useExternalProxies:
 
             # Load in cached copy of external proxies so we can diff against them
@@ -944,257 +920,44 @@
 
         yield self.cache.setPopulatedMarker()
 
-        if useLock:
-            self.log_info("Releasing lock")
-            yield self.cache.releaseLock()
-
         self.log_info("Group memberships cache updated")
 
         returnValue((fast, len(members), len(changedMembers)))
 
 
+class GroupCacherPollingWork(WorkItem, fromTable(schema.GROUP_CACHER_POLLING_WORK)):
 
-class GroupMembershipCacherOptions(Options):
-    optParameters = [[
-        "config", "f", DEFAULT_CONFIG_FILE, "Path to configuration file."
-    ]]
+    group = "group_cacher_polling"
 
-    def __init__(self, *args, **kwargs):
-        super(GroupMembershipCacherOptions, self).__init__(*args, **kwargs)
-
-        self.overrides = {}
-
-
-    def _coerceOption(self, configDict, key, value):
-        """
-        Coerce the given C{val} to type of C{configDict[key]}
-        """
-        if key in configDict:
-            if isinstance(configDict[key], bool):
-                value = value == "True"
-
-            elif isinstance(configDict[key], (int, float, long)):
-                value = type(configDict[key])(value)
-
-            elif isinstance(configDict[key], (list, tuple)):
-                value = value.split(',')
-
-            elif isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Dict options not supported on the command line"
-                )
-
-            elif value == 'None':
-                value = None
-
-        return value
-
-
-    def _setOverride(self, configDict, path, value, overrideDict):
-        """
-        Set the value at path in configDict
-        """
-        key = path[0]
-
-        if len(path) == 1:
-            overrideDict[key] = self._coerceOption(configDict, key, value)
-            return
-
-        if key in configDict:
-            if not isinstance(configDict[key], dict):
-                raise UsageError(
-                    "Found intermediate path element that is not a dictionary"
-                )
-
-            if key not in overrideDict:
-                overrideDict[key] = {}
-
-            self._setOverride(
-                configDict[key], path[1:],
-                value, overrideDict[key]
-            )
-
-
-    def opt_option(self, option):
-        """
-        Set an option to override a value in the config file. True, False, int,
-        and float options are supported, as well as comma seperated lists. Only
-        one option may be given for each --option flag, however multiple
-        --option flags may be specified.
-        """
-
-        if "=" in option:
-            path, value = option.split('=')
-            self._setOverride(
-                DEFAULT_CONFIG,
-                path.split('/'),
-                value,
-                self.overrides
-            )
-        else:
-            self.opt_option('%s=True' % (option,))
-
-    opt_o = opt_option
-
-    def postOptions(self):
-        config.load(self['config'])
-        config.updateDefaults(self.overrides)
-        self.parent['pidfile'] = config.PIDFile
-
-
-
-class GroupMembershipCacherService(service.Service, LoggingMixIn):
-    """
-    Service to update the group membership cache at a configured interval
-    """
-
-    def __init__(self, proxyDB, directory, namespace, updateSeconds,
-        expireSeconds, lockSeconds, reactor=None, updateMethod=None,
-        useExternalProxies=False):
-
-        if updateSeconds >= expireSeconds:
-            expireSeconds = updateSeconds * 2
-            self.log_warn("Configuration warning: GroupCaching.ExpireSeconds needs to be longer than UpdateSeconds; setting to %d seconds" % (expireSeconds,))
-
-        self.updater = GroupMembershipCacheUpdater(proxyDB, directory,
-            expireSeconds, lockSeconds, namespace=namespace,
-            useExternalProxies=useExternalProxies)
-
-        if reactor is None:
-            from twisted.internet import reactor
-        self.reactor = reactor
-
-        self.updateSeconds = updateSeconds
-        self.nextUpdate = None
-        self.updateInProgress = False
-        self.updateAwaiting = False
-
-        if updateMethod:
-            self.updateMethod = updateMethod
-        else:
-            self.updateMethod = self.updater.updateCache
-
-
-    def startService(self):
-        self.previousHandler = signal.signal(signal.SIGHUP, self.sighupHandler)
-        self.log_warn("Starting group membership cacher service")
-        service.Service.startService(self)
-        return self.update()
-
-
-    def sighupHandler(self, num, frame):
-        self.reactor.callFromThread(self.update)
-
-
-    def stopService(self):
-        signal.signal(signal.SIGHUP, self.previousHandler)
-        self.log_warn("Stopping group membership cacher service")
-        service.Service.stopService(self)
-        if self.nextUpdate is not None:
-            self.nextUpdate.cancel()
-            self.nextUpdate = None
-
-
     @inlineCallbacks
-    def update(self):
-        """
-        A wrapper around updateCache, this method manages the scheduling of the
-        subsequent update, as well as prevents multiple updates from running
-        simultaneously, which could otherwise happen because SIGHUP now triggers
-        an update on demand.  If update is called while an update is in progress,
-        as soon as the first update is finished a new one is started.  Otherwise,
-        when an update finishes and there is not another one waiting, the next
-        update is scheduled for updateSeconds in the future.
+    def doWork(self):
 
-        @return: True if an update was already in progress, False otherwise
-        @rtype: C{bool}
-        """
+        # Delete all other work items
+        yield Delete(From=self.table, Where=None).on(self.transaction)
 
-        self.log_debug("Group membership update called")
+        groupCacher = self.transaction._groupCacher
+        if groupCacher is not None:
+            try:
+                yield groupCacher.updateCache()
+            except Exception, e:
+                log.error("Failed to update group membership cache (%s)" % (e,))
+            finally:
+                notBefore = (datetime.datetime.utcnow() +
+                    datetime.timedelta(seconds=groupCacher.updateSeconds))
+                log.debug("Scheduling next group cacher update: %s" % (notBefore,))
+                yield self.transaction.enqueue(GroupCacherPollingWork,
+                    notBefore=notBefore)
 
-        # A call to update while an update is in progress sets the updateAwaiting flag
-        # so that an update happens again right after the current one is complete.
-        if self.updateInProgress:
-            self.updateAwaiting = True
-            returnValue(True)
 
-        self.nextUpdate = None
-        self.updateInProgress = True
-        self.updateAwaiting = False
-        try:
-            yield self.updateMethod()
-        finally:
-            self.updateInProgress = False
-            if self.updateAwaiting:
-                self.log_info("Performing group membership update")
-                yield self.update()
-            else:
-                self.log_info("Scheduling next group membership update")
-                self.nextUpdate = self.reactor.callLater(self.updateSeconds,
-                    self.update)
-        returnValue(False)
+ at inlineCallbacks
+def scheduleNextGroupCachingUpdate(store, seconds):
+    txn = store.newTransaction()
+    notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
+    log.debug("Scheduling next group cacher update: %s" % (notBefore,))
+    yield txn.enqueue(GroupCacherPollingWork, notBefore=notBefore)
+    yield txn.commit()
 
 
-
-class GroupMembershipCacherServiceMaker(LoggingMixIn):
-    """
-    Configures and returns a GroupMembershipCacherService
-    """
-    implements(IPlugin, service.IServiceMaker)
-
-    tapname = "caldav_groupcacher"
-    description = "Group Membership Cacher"
-    options = GroupMembershipCacherOptions
-
-    def makeService(self, options):
-        try:
-            from setproctitle import setproctitle
-        except ImportError:
-            pass
-        else:
-            setproctitle("CalendarServer [Group Cacher]")
-
-        # Setup the directory
-        from calendarserver.tap.util import directoryFromConfig
-        directory = directoryFromConfig(config)
-
-        # We have to set cacheNotifierFactory otherwise group cacher can't
-        # invalidate the cache tokens for principals whose membership has
-        # changed
-        if config.EnableResponseCache and config.Memcached.Pools.Default.ClientEnabled:
-            from twistedcaldav.directory.principal import DirectoryPrincipalResource
-            from twistedcaldav.cache import MemcacheChangeNotifier
-            DirectoryPrincipalResource.cacheNotifierFactory = MemcacheChangeNotifier
-
-        # Setup the ProxyDB Service
-        proxydbClass = namedClass(config.ProxyDBService.type)
-
-        self.log_warn("Configuring proxydb service of type: %s" % (proxydbClass,))
-
-        try:
-            proxyDB = proxydbClass(**config.ProxyDBService.params)
-        except IOError:
-            self.log_error("Could not start proxydb service")
-            raise
-
-        # Setup memcached pools
-        memcachepool.installPools(
-            config.Memcached.Pools,
-            config.Memcached.MaxClients,
-        )
-
-        cacherService = GroupMembershipCacherService(proxyDB, directory,
-            config.GroupCaching.MemcachedPool,
-            config.GroupCaching.UpdateSeconds,
-            config.GroupCaching.ExpireSeconds,
-            config.GroupCaching.LockSeconds,
-            useExternalProxies=config.GroupCaching.UseExternalProxies
-            )
-
-        return cacherService
-
-
-
 def diffAssignments(old, new):
     """
     Compare two proxy assignment lists and return their differences in the form of

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/test/test_directory.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/directory/test/test_directory.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -15,13 +15,12 @@
 ##
 
 from twisted.internet.defer import inlineCallbacks
-from twisted.internet.task import Clock
 from twisted.python.filepath import FilePath
 
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.test.util import xmlFile, augmentsFile, proxiesFile, dirTest
 from twistedcaldav.config import config
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, GroupMembershipCacherService, GroupMembershipCache, GroupMembershipCacheUpdater, diffAssignments
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, GroupMembershipCache, GroupMembershipCacheUpdater, diffAssignments
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
 from twistedcaldav.directory import augment, calendaruserproxy
@@ -121,46 +120,7 @@
         self.count += 1
 
 
-    @inlineCallbacks
-    def test_groupMembershipCacherService(self):
-        """
-        Instantiate a GroupMembershipCacherService and make sure its update
-        method fires at the right interval, in this case 30 seconds.  The
-        updateMethod keyword arg is purely for testing purposes, so we can
-        directly detect it getting called in this test.
-        """
-        clock = Clock()
-        self.count = 0
 
-        # Deliberately set the expireSeconds lower than updateSeconds to verify
-        # expireSeconds gets set to 2 * updateSeconds in that scenario
-
-        service = GroupMembershipCacherService(
-            None, None, "Testing", 30, 20, 30, reactor=clock,
-            updateMethod=self._updateMethod)
-
-        # expireSeconds = 2 * 30 updateSeconds
-        self.assertEquals(service.updater.cache.expireSeconds, 60)
-
-        yield service.startService()
-
-        self.assertEquals(self.count, 1)
-        clock.advance(29)
-        self.assertEquals(self.count, 1)
-        clock.advance(1)
-        self.assertEquals(self.count, 2)
-
-        service.stopService()
-
-        service.updateInProgress = True
-        self.assertTrue((yield service.update()))
-        self.assertTrue(service.updateAwaiting)
-
-        service.updateInProgress = False
-        self.assertFalse((yield service.update()))
-        self.assertFalse(service.updateAwaiting)
-
-
     def test_expandedMembers(self):
         """
         Make sure expandedMembers( ) returns a complete, flattened set of
@@ -279,17 +239,6 @@
             )
         )
 
-        # Prevent an update by locking the cache
-        acquiredLock = (yield cache.acquireLock())
-        self.assertTrue(acquiredLock)
-        self.assertEquals((False, 0), (yield updater.updateCache()))
-
-        # You can't lock when already locked:
-        acquiredLockAgain = (yield cache.acquireLock())
-        self.assertFalse(acquiredLockAgain)
-
-        # Allow an update by unlocking the cache
-        yield cache.releaseLock()
         self.assertEquals((False, 9, 9), (yield updater.updateCache()))
 
         # Verify cache is populated:
@@ -686,10 +635,6 @@
         # time), but since the snapshot doesn't exist we fault in from the
         # directory (fast now is False), and snapshot will get created
 
-        # Note that because fast=True and isPopulated() is False, locking is
-        # ignored:
-        yield cache.acquireLock()
-
         self.assertFalse((yield cache.isPopulated()))
         fast, numMembers, numChanged = (yield updater.updateCache(fast=True))
         self.assertEquals(fast, False)
@@ -698,8 +643,6 @@
         self.assertTrue(snapshotFile.exists())
         self.assertTrue((yield cache.isPopulated()))
 
-        yield cache.releaseLock()
-
         # Try another fast update where the snapshot already exists (as in a
         # server-restart scenario), which will only read from the snapshot
         # as indicated by the return value for "fast".  Note that the cache

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/ical.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/ical.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -2979,8 +2979,7 @@
                 # Check that we can lookup this calendar user address - if not
                 # we cannot do anything with it
                 cuaddr = normalizeCUAddr(prop.value())
-                name, guid, cuaddrs = lookupFunction(cuaddr, principalFunction,
-                    config)
+                name, guid, cuaddrs = lookupFunction(cuaddr, principalFunction, config)
                 if guid is None:
                     continue
 
@@ -2989,6 +2988,11 @@
                 if oldemail:
                     oldemail = "mailto:%s" % (oldemail,)
 
+                # Get any CN parameter
+                oldCN = prop.parameterValue("CN")
+
+                cutype = prop.parameterValue("CUTYPE")
+
                 if toUUID:
                     # Store the original CUA if http(s) or /path:
                     if config.Scheduling.Options.V1Compatibility:
@@ -3049,9 +3053,17 @@
                     if newaddr:
                         prop.setValue(newaddr)
 
-                # Always re-write the CN parameter
+                # Re-write the CN parameter
                 if name:
-                    prop.setParameter("CN", name)
+                    if name != oldCN:
+                        prop.setParameter("CN", name)
+
+                        # Also adjust any previously matching location property
+                        if cutype == "ROOM":
+                            location = component.getProperty("LOCATION")
+                            if location is not None:
+                                if location.value() == oldCN:
+                                    location.setValue(name)
                 else:
                     prop.removeParameter("CN")
 

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/method/put_common.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/method/put_common.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -203,6 +203,7 @@
         self.access = None
         self.hasPrivateComments = False
         self.isScheduleResource = False
+        self.dataChanged = False
 
 
     @inlineCallbacks
@@ -365,6 +366,9 @@
             # Check that moves to shared calendars are OK
             yield self.validCopyMoveOperation()
 
+            # Check location/resource organizer requirement
+            yield self.validLocationResourceOrganizer()
+
             # Check access
             if self.destinationcal and config.EnablePrivateEvents:
                 result = (yield self.validAccess())
@@ -662,6 +666,53 @@
 
 
     @inlineCallbacks
+    def validLocationResourceOrganizer(self):
+        """
+        If the calendar owner is a location or resource, check whether an ORGANIZER property is required.
+        """
+
+        if not self.internal_request:
+            originatorPrincipal = (yield self.destination.ownerPrincipal(self.request))
+            cutype = originatorPrincipal.getCUType() if originatorPrincipal is not None else "INDIVIDUAL"
+            organizer = self.calendar.getOrganizer()
+
+            # Check for an allowed change
+            if organizer is None and (
+                cutype == "ROOM" and not config.Scheduling.Options.AllowLocationWithoutOrganizer or
+                cutype == "RESOURCE" and not config.Scheduling.Options.AllowResourceWithoutOrganizer):
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (calendarserver_namespace, "valid-organizer"),
+                    "Organizer required in calendar data",
+                ))
+
+            # Check for tracking the modifier
+            if organizer is None and (
+                cutype == "ROOM" and config.Scheduling.Options.TrackUnscheduledLocationData or
+                cutype == "RESOURCE" and config.Scheduling.Options.TrackUnscheduledResourceData):
+
+                # Find current principal
+                authz = None
+                authz_principal = self.destinationparent.currentPrincipal(self.request).children[0]
+                if isinstance(authz_principal, davxml.HRef):
+                    principalURL = str(authz_principal)
+                    if principalURL:
+                        authz = (yield self.request.locateResource(principalURL))
+
+                if authz is not None:
+                    prop = Property("X-CALENDARSERVER-MODIFIED-BY", "urn:uuid:%s" % (authz.record.guid,))
+                    prop.setParameter("CN", authz.displayName())
+                    for candidate in authz.calendarUserAddresses():
+                        if candidate.startswith("mailto:"):
+                            prop.setParameter("EMAIL", candidate[7:])
+                            break
+                    self.calendar.replacePropertyInAllComponents(prop)
+                else:
+                    self.calendar.removeAllPropertiesWithName("X-CALENDARSERVER-MODIFIED-BY")
+                self.dataChanged = True
+
+
+    @inlineCallbacks
     def preservePrivateComments(self):
         # Check for private comments on the old resource and the new resource and re-insert
         # ones that are lost.
@@ -769,7 +820,6 @@
         """
 
         # Only relevant if calendar is sharee collection
-        changed = False
         if self.destinationparent.isShareeResource():
 
             # Get all X-APPLE-DROPBOX's and ATTACH's that are http URIs
@@ -813,59 +863,52 @@
                     uri = uriNormalize(xdropbox.value())
                     if uri:
                         xdropbox.setValue(uri)
-                        changed = True
+                        self.dataChanged = True
                 for attachment in attachments:
                     uri = uriNormalize(attachment.value())
                     if uri:
                         attachment.setValue(uri)
-                        changed = True
+                        self.dataChanged = True
 
-        returnValue(changed)
 
-
     def processAlarms(self):
         """
         Remove duplicate alarms. Add a default alarm if required.
-
-        @return: indicate whether a change was made
-        @rtype: C{bool}
         """
 
         # Remove duplicate alarms
-        changed = False
-        if config.RemoveDuplicateAlarms:
-            changed = self.calendar.hasDuplicateAlarms(doFix=True)
+        if config.RemoveDuplicateAlarms and self.calendar.hasDuplicateAlarms(doFix=True):
+            self.dataChanged = True
 
         # Only if feature enabled
         if not config.EnableDefaultAlarms:
-            return changed
+            return
 
         # Check that we are creating and this is not the inbox
         if not self.destinationcal or self.destination.exists() or self.isiTIP:
-            return changed
+            return
 
         # Never add default alarms to calendar data in shared calendars
         if self.destinationparent.isShareeResource():
-            return changed
+            return
 
         # Add default alarm for VEVENT and VTODO only
         mtype = self.calendar.mainType().upper()
         if self.calendar.mainType().upper() not in ("VEVENT", "VTODO"):
-            return changed
+            return
         vevent = mtype == "VEVENT"
 
         # Check timed or all-day
         start, _ignore_end = self.calendar.mainComponent(allow_multiple=True).getEffectiveStartEnd()
         if start is None:
             # Yes VTODOs might have no DTSTART or DUE - in this case we do not add a default
-            return changed
+            return
         timed = not start.isDateOnly()
 
         # See if default exists and add using appropriate logic
         alarm = self.destinationparent.getDefaultAlarm(vevent, timed)
-        if alarm:
-            changed = self.calendar.addAlarms(alarm)
-        return changed
+        if alarm and self.calendar.addAlarms(alarm):
+            self.dataChanged = True
 
 
     @inlineCallbacks
@@ -1221,14 +1264,14 @@
             yield self.replaceMissingToDoProperties()
 
             # Handle sharing dropbox normalization
-            dropboxChanged = (yield self.dropboxPathNormalization())
+            yield self.dropboxPathNormalization()
 
             # Pre-process managed attachments
             if not self.internal_request and not self.attachmentProcessingDone:
                 managed_copied, managed_removed = (yield self.destination.preProcessManagedAttachments(self.calendar))
 
             # Default/duplicate alarms
-            alarmChanged = self.processAlarms()
+            self.processAlarms()
 
             # Do scheduling
             implicit_result = (yield self.doImplicitScheduling())
@@ -1276,7 +1319,7 @@
                 yield self.destination.postProcessManagedAttachments(managed_copied, managed_removed)
 
             # Must not set ETag in response if data changed
-            if did_implicit_action or dropboxChanged or alarmChanged:
+            if did_implicit_action or self.dataChanged:
                 def _removeEtag(request, response):
                     response.headers.removeHeader('etag')
                     return response

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/scheduling/imip/inbound.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/scheduling/imip/inbound.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -120,22 +120,25 @@
         self.point = GAIEndpoint(self.reactor, settings.Server,
             settings.Port, contextFactory=contextFactory)
 
-    def startService(self):
-        return self.scheduleNextPoll(seconds=0)
 
-
     def fetchMail(self):
         return self.point.connect(self.factory(self.settings, self.mailReceiver))
 
+
     @inlineCallbacks
     def scheduleNextPoll(self, seconds=None):
         if seconds is None:
             seconds = self.settings["PollingSeconds"]
-        txn = self.store.newTransaction()
-        notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
-        yield txn.enqueue(IMIPPollingWork, notBefore=notBefore)
-        yield txn.commit()
+        yield scheduleNextMailPoll(self.store, seconds)
+        
 
+ at inlineCallbacks
+def scheduleNextMailPoll(store, seconds):
+    txn = store.newTransaction()
+    notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
+    log.debug("Scheduling next mail poll: %s" % (notBefore,))
+    yield txn.enqueue(IMIPPollingWork, notBefore=notBefore)
+    yield txn.commit()
 
 
 class MailReceiver(object):

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/stdconfig.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/stdconfig.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -694,6 +694,10 @@
             "AllowGroupAsOrganizer"               : False, # Allow groups to be Organizers
             "AllowLocationAsOrganizer"            : False, # Allow locations to be Organizers
             "AllowResourceAsOrganizer"            : False, # Allow resources to be Organizers
+            "AllowLocationWithoutOrganizer"       : True, # Allow locations to have events without an Organizer
+            "AllowResourceWithoutOrganizer"       : True, # Allow resources to have events without an Organizer
+            "TrackUnscheduledLocationData"        : True, # Track who the last modifier of an unscheduled location event is
+            "TrackUnscheduledResourceData"        : True, # Track who the last modifier of an unscheduled resource event is
             "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
@@ -927,7 +931,6 @@
         "MemcachedPool" : "Default",
         "UpdateSeconds" : 300,
         "ExpireSeconds" : 3600,
-        "LockSeconds" : 300,
         "EnableUpdater" : True,
         "UseExternalProxies" : False,
     },
@@ -1035,6 +1038,7 @@
         return configDict
 
 
+
 def _expandPath(path):
     if '$' in path:
         return path.replace('$', getfqdn())

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_icalendar.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_icalendar.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -7432,6 +7432,165 @@
             "base64-aHR0cDovL2V4YW1wbGUuY29tL3ByaW5jaXBhbHMvdXNlcnMvYnV6")
 
 
+    def test_normalizeCalendarUserAddressesAndLocationChange(self):
+        """
+        Ensure http(s) and /path CUA values are tucked away into the property
+        using CALENDARSERVER-OLD-CUA parameter.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+DTSTART:20071114T000000Z
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ATTENDEE:/principals/users/foo
+ATTENDEE:http://example.com/principals/users/bar
+ATTENDEE;CN=Buzz;CUTYPE=ROOM:http://example.com/principals/locations/buzz
+LOCATION:Buzz
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data)
+
+
+        def lookupFunction(cuaddr, ignored1, ignored2):
+            return {
+                "/principals/users/foo" : (
+                    "Foo",
+                    "foo",
+                    ("urn:uuid:foo",)
+                ),
+                "http://example.com/principals/users/bar" : (
+                    "Bar",
+                    "bar",
+                    ("urn:uuid:bar",)
+                ),
+                "http://example.com/principals/locations/buzz" : (
+                    "{Restricted} Buzz",
+                    "buzz",
+                    ("urn:uuid:buzz",)
+                ),
+            }[cuaddr]
+
+        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+
+        # Location value changed
+        prop = component.mainComponent().getProperty("LOCATION")
+        self.assertEquals(prop.value(), "{Restricted} Buzz")
+        prop = component.getAttendeeProperty(("urn:uuid:buzz",))
+        self.assertEquals("urn:uuid:buzz", prop.value())
+        self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
+
+
+    def test_normalizeCalendarUserAddressesAndLocationNoChange(self):
+        """
+        Ensure http(s) and /path CUA values are tucked away into the property
+        using CALENDARSERVER-OLD-CUA parameter.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+DTSTART:20071114T000000Z
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ATTENDEE:/principals/users/foo
+ATTENDEE:http://example.com/principals/users/bar
+ATTENDEE;CN=Buzz;CUTYPE=ROOM:http://example.com/principals/locations/buzz
+LOCATION:Fuzz
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data)
+
+
+        def lookupFunction(cuaddr, ignored1, ignored2):
+            return {
+                "/principals/users/foo" : (
+                    "Foo",
+                    "foo",
+                    ("urn:uuid:foo",)
+                ),
+                "http://example.com/principals/users/bar" : (
+                    "Bar",
+                    "bar",
+                    ("urn:uuid:bar",)
+                ),
+                "http://example.com/principals/locations/buzz" : (
+                    "{Restricted} Buzz",
+                    "buzz",
+                    ("urn:uuid:buzz",)
+                ),
+            }[cuaddr]
+
+        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+
+        # Location value changed
+        prop = component.mainComponent().getProperty("LOCATION")
+        self.assertEquals(prop.value(), "Fuzz")
+        prop = component.getAttendeeProperty(("urn:uuid:buzz",))
+        self.assertEquals("urn:uuid:buzz", prop.value())
+        self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
+
+
+    def test_normalizeCalendarUserAddressesAndLocationNoChangeOtherCUType(self):
+        """
+        Ensure http(s) and /path CUA values are tucked away into the property
+        using CALENDARSERVER-OLD-CUA parameter.
+        """
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+DTSTART:20071114T000000Z
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ATTENDEE:/principals/users/foo
+ATTENDEE:http://example.com/principals/users/bar
+ATTENDEE;CN=Buzz;CUTYPE=RESOURCE:http://example.com/principals/locations/buzz
+LOCATION:Buzz
+DTSTAMP:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data)
+
+
+        def lookupFunction(cuaddr, ignored1, ignored2):
+            return {
+                "/principals/users/foo" : (
+                    "Foo",
+                    "foo",
+                    ("urn:uuid:foo",)
+                ),
+                "http://example.com/principals/users/bar" : (
+                    "Bar",
+                    "bar",
+                    ("urn:uuid:bar",)
+                ),
+                "http://example.com/principals/locations/buzz" : (
+                    "{Restricted} Buzz",
+                    "buzz",
+                    ("urn:uuid:buzz",)
+                ),
+            }[cuaddr]
+
+        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+
+        # Location value changed
+        prop = component.mainComponent().getProperty("LOCATION")
+        self.assertEquals(prop.value(), "Buzz")
+        prop = component.getAttendeeProperty(("urn:uuid:buzz",))
+        self.assertEquals("urn:uuid:buzz", prop.value())
+        self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
+
+
     def test_serializationCaching(self):
 
         data = """BEGIN:VCALENDAR

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_validation.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_validation.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/test/test_validation.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -23,6 +23,7 @@
 # to address this to use system twisted.
 from twext.web2.test.test_server import SimpleRequest
 from twext.web2.http import HTTPError
+from twext.web2.resource import Resource
 
 from twistedcaldav.config import config
 from twistedcaldav.ical import Component, Property
@@ -31,16 +32,20 @@
 from twistedcaldav.resource import CalDAVResource
 
 class InMemoryCalendarObjectResource(CalDAVResource):
-    
+
     def exists(self):
         return hasattr(self, "_data") and self._data is not None
 
+
     def iCalendarForUser(self, user):
         return self._data
-    
+
+
     def setData(self, data):
         self._data = data
 
+
+
 class TestCopyMoveValidation(TestCase):
     """
     Tests for the validation code in L{twistedcaldav.method.put_common}.
@@ -52,11 +57,12 @@
         """
 
         self.destination = InMemoryCalendarObjectResource()
-        self.destination.name = lambda : '1'
+        self.destination.name = lambda : 'bar'
         self.destinationParent = CalDAVResource()
-        self.destinationParent.name = lambda : '2'
+        self.destinationParent.name = lambda : 'foo'
         self.destinationParent.isSupportedComponent = lambda x: True
 
+
     def _getSampleCalendar(self):
         return Component.fromString("""BEGIN:VCALENDAR
 VERSION:2.0
@@ -72,9 +78,13 @@
 END:VCALENDAR
 """)
 
+
     def _getStorer(self, calendar):
         self.sampleCalendar = calendar
         req = SimpleRequest(None, "COPY", "http://example.com/foo/bar")
+        req._rememberResource(self.destination, "/foo/bar")
+        req._rememberResource(self.destinationParent, "/foo/")
+        req._rememberResource(Resource(), "/")
         self.storer = StoreCalendarObjectResource(
             req,
             destination=self.destination,
@@ -83,7 +93,8 @@
             calendar=self.sampleCalendar
         )
         return self.storer
-                
+
+
     @inlineCallbacks
     def test_simpleValidRequest(self):
         """
@@ -110,7 +121,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -143,7 +154,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance - 5):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -155,7 +166,7 @@
         config.MaxAttendeesPerInstance -= 10
         eventComponent.addProperty(
             Property("ATTENDEE", "mailto:user-extra at example.com"))
-        
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError, err:
@@ -187,7 +198,7 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance - 5):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
 
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
@@ -197,7 +208,7 @@
 
         # Now reduce the limit and try to store without any additional attendees.
         config.MaxAttendeesPerInstance -= 10
-        
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError:
@@ -209,8 +220,8 @@
         eventComponent = list(self.sampleCalendar.subcomponents())[0]
         for x in xrange(config.MaxAttendeesPerInstance + 2):
             eventComponent.addProperty(
-                Property("ATTENDEE", "mailto:user%d at example.com" % (x+3,)))
-        
+                Property("ATTENDEE", "mailto:user%d at example.com" % (x + 3,)))
+
         try:
             yield self._getStorer(self.sampleCalendar).fullValidation()
         except HTTPError:

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/upgrade.py	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/twistedcaldav/upgrade.py	2013-04-16 22:19:02 UTC (rev 11051)
@@ -39,7 +39,9 @@
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
 from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
-from twistedcaldav.directory.directory import DirectoryService, GroupMembershipCacheUpdater
+from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.directory.directory import GroupMembershipCacheUpdater
+from twistedcaldav.directory.directory import scheduleNextGroupCachingUpdate
 from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
 from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
@@ -67,6 +69,7 @@
 
 from twext.python.parallel import Parallelizer
 from twistedcaldav.scheduling.imip.mailgateway import migrateTokensToStore
+from twistedcaldav.scheduling.imip.inbound import scheduleNextMailPoll
 
 
 deadPropertyXattrPrefix = namedAny(
@@ -1069,11 +1072,14 @@
                 proxydb = proxydbClass(**self.config.ProxyDBService.params)
 
             updater = GroupMembershipCacheUpdater(proxydb,
-                directory, self.config.GroupCaching.ExpireSeconds,
-                self.config.GroupCaching.LockSeconds,
+                directory,
+                self.config.GroupCaching.UpdateSeconds,
+                self.config.GroupCaching.ExpireSeconds,
                 namespace=self.config.GroupCaching.MemcachedPool,
                 useExternalProxies=self.config.GroupCaching.UseExternalProxies)
             yield updater.updateCache(fast=True)
+            # Set in motion the work queue based updates:
+            yield scheduleNextGroupCachingUpdate(self.store, 0)
 
             uid, gid = getCalendarServerIDs(self.config)
             dbPath = os.path.join(self.config.DataRoot, "proxies.sqlite")
@@ -1087,6 +1093,9 @@
 
         # Migrate mail tokens from sqlite to store
         yield migrateTokensToStore(self.config.DataRoot, self.store)
+        # Set mail polling in motion
+        if self.config.Scheduling.iMIP.Enabled:
+            yield scheduleNextMailPoll(self.store, 0)
 
 
 

Modified: CalendarServer/branches/users/glyph/sharedgroups-2/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/glyph/sharedgroups-2/txdav/common/datastore/sql_schema/current.sql	2013-04-16 22:18:25 UTC (rev 11050)
+++ CalendarServer/branches/users/glyph/sharedgroups-2/txdav/common/datastore/sql_schema/current.sql	2013-04-16 22:19:02 UTC (rev 11051)
@@ -618,7 +618,16 @@
   PUSH_ID                       varchar(255) not null
 );
 
+-----------------
+-- GroupCacher --
+-----------------
 
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
 --------------------
 -- Schema Version --
 --------------------
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130416/95957dbd/attachment-0001.html>


More information about the calendarserver-changes mailing list