[CalendarServer-changes] [12243] CalendarServer/branches/release/CalendarServer-5.2-dev

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:20:26 PDT 2014


Revision: 12243
          http://trac.calendarserver.org//changeset/12243
Author:   sagen at apple.com
Date:     2014-01-06 14:03:14 -0800 (Mon, 06 Jan 2014)
Log Message:
-----------
Port structured location feature to branch

Modified Paths:
--------------
    CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/gateway.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/principals.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/gateway/caldavd.plist
    CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/test_gateway.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/augments-test.xml
    CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/resources-test.xml
    CalendarServer/branches/release/CalendarServer-5.2-dev/conf/caldavd-test.plist
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/datafilters/peruserdata.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/augment.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/directory.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/ldapdirectory.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_aggregate.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_ldapdirectory.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_xmlfile.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/util.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlaccountsparser.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlfile.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/stdconfig.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/sql.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/util.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/datastore/test/util.py
    CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/idirectoryservice.py

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/gateway.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/gateway.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -22,7 +22,7 @@
 import sys
 import xml
 
-from twext.python.plistlib import readPlistFromString, writePlistToString
+from plistlib import readPlistFromString, writePlistToString
 
 from twisted.internet.defer import inlineCallbacks, succeed
 from twistedcaldav.directory.directory import DirectoryError
@@ -32,13 +32,15 @@
     principalForPrincipalID, proxySubprincipal, addProxy, removeProxy,
     ProxyError, ProxyWarning, autoDisableMemcached
 )
-from calendarserver.tools.principals import getProxies, setProxies, updateRecord
+from calendarserver.tools.principals import (
+    getProxies, setProxies, updateRecord, attrMap
+)
 from calendarserver.tools.purge import WorkerService, PurgeOldEventsService, DEFAULT_BATCH_SIZE, DEFAULT_RETAIN_DAYS
 from calendarserver.tools.cmdline import utilityMain
 
 from pycalendar.datetime import PyCalendarDateTime
 
-from twistedcaldav.config import config, ConfigDict 
+from twistedcaldav.config import config, ConfigDict
 
 from calendarserver.tools.config import WRITABLE_CONFIG_KEYS, setKeyPath, getKeyPath, flattenDictionary, WritableConfig
 
@@ -140,25 +142,6 @@
     utilityMain(configFileName, RunnerService, verbose=debug)
 
 
-attrMap = {
-    'GeneratedUID' : { 'attr' : 'guid', },
-    'RealName' : { 'attr' : 'fullName', },
-    'RecordName' : { 'attr' : 'shortNames', },
-    'Comment' : { 'extras' : True, 'attr' : 'comment', },
-    'Description' : { 'extras' : True, 'attr' : 'description', },
-    'Type' : { 'extras' : True, 'attr' : 'type', },
-    'Capacity' : { 'extras' : True, 'attr' : 'capacity', },
-    'Building' : { 'extras' : True, 'attr' : 'building', },
-    'Floor' : { 'extras' : True, 'attr' : 'floor', },
-    'Street' : { 'extras' : True, 'attr' : 'street', },
-    'City' : { 'extras' : True, 'attr' : 'city', },
-    'State' : { 'extras' : True, 'attr' : 'state', },
-    'ZIP' : { 'extras' : True, 'attr' : 'zip', },
-    'Country' : { 'extras' : True, 'attr' : 'country', },
-    'Phone' : { 'extras' : True, 'attr' : 'phone', },
-    'AutoSchedule' : { 'attr' : 'autoSchedule', },
-    'AutoAcceptGroup' : { 'attr' : 'autoAcceptGroup', },
-}
 
 class Runner(object):
 
@@ -217,9 +200,9 @@
             self.respondWithError("Command failed: '%s'" % (str(e),))
             raise
 
+
     # Locations
 
-
     def command_getLocationList(self, command):
         self.respondWithRecordsOfTypes(self.dir, command, ["locations"])
 
@@ -265,6 +248,7 @@
 
     command_getResourceAttributes = command_getLocationAttributes
 
+
     @inlineCallbacks
     def command_setLocationAttributes(self, command):
 
@@ -305,9 +289,9 @@
             return
         self.respondWithRecordsOfTypes(self.dir, command, ["locations"])
 
+
     # Resources
 
-
     def command_getResourceList(self, command):
         self.respondWithRecordsOfTypes(self.dir, command, ["resources"])
 
@@ -373,11 +357,72 @@
             return
         self.respondWithRecordsOfTypes(self.dir, command, ["resources"])
 
-        
+
     def command_getLocationAndResourceList(self, command):
         self.respondWithRecordsOfTypes(self.dir, command, ["locations", "resources"])
 
 
+    # Addresses
+
+    def command_getAddressList(self, command):
+        self.respondWithRecordsOfTypes(self.dir, command, ["addresses"])
+
+
+    @inlineCallbacks
+    def command_createAddress(self, command):
+        kwargs = {}
+        for key, info in attrMap.iteritems():
+            if key in command:
+                kwargs[info['attr']] = command[key]
+
+        try:
+            yield updateRecord(True, self.dir, "addresses", **kwargs)
+        except DirectoryError, e:
+            self.respondWithError(str(e))
+            return
+
+        self.respondWithRecordsOfTypes(self.dir, command, ["addresses"])
+
+
+    def command_getAddressAttributes(self, command):
+        guid = command['GeneratedUID']
+        record = self.dir.recordWithGUID(guid)
+        if record is None:
+            self.respondWithError("Principal not found: %s" % (guid,))
+            return
+        recordDict = recordToDict(record)
+        self.respond(command, recordDict)
+        return succeed(None)
+
+
+    @inlineCallbacks
+    def command_setAddressAttributes(self, command):
+        kwargs = {}
+        for key, info in attrMap.iteritems():
+            if key in command:
+                kwargs[info['attr']] = command[key]
+        try:
+            yield updateRecord(False, self.dir, "addresses", **kwargs)
+        except DirectoryError, e:
+            self.respondWithError(str(e))
+            return
+
+        yield self.command_getAddressAttributes(command)
+
+
+    def command_deleteAddress(self, command):
+        kwargs = {}
+        for key, info in attrMap.iteritems():
+            if key in command:
+                kwargs[info['attr']] = command[key]
+        try:
+            self.dir.destroyRecord("addresses", **kwargs)
+        except DirectoryError, e:
+            self.respondWithError(str(e))
+            return
+        self.respondWithRecordsOfTypes(self.dir, command, ["addresses"])
+
+
     # Config
 
     def command_readConfig(self, command):
@@ -424,10 +469,8 @@
             self.command_readConfig(command)
 
 
-
     # Proxies
 
-
     @inlineCallbacks
     def command_listWriteProxies(self, command):
         principal = principalForPrincipalID(command['Principal'], directory=self.dir)
@@ -545,7 +588,6 @@
         self.respond(command, {'EventsRemoved' : eventCount, "RetainDays" : retainDays})
 
 
-
     @inlineCallbacks
     def respondWithProxies(self, directory, command, principal, proxyType):
         proxies = []
@@ -562,7 +604,6 @@
         })
 
 
-
     def respondWithRecordsOfTypes(self, directory, command, recordTypes):
         result = []
         for recordType in recordTypes:
@@ -572,7 +613,6 @@
         self.respond(command, result)
 
 
-
     def respond(self, command, result):
         self.output.write(writePlistToString({'command' : command['command'], 'result' : result}))
 
@@ -581,6 +621,7 @@
         self.output.write(writePlistToString({'error' : msg, }))
 
 
+
 def recordToDict(record):
     recordDict = {}
     for key, info in attrMap.iteritems():
@@ -596,6 +637,8 @@
             pass
     return recordDict
 
+
+
 def respondWithError(msg, status=1):
     sys.stdout.write(writePlistToString({'error' : msg, }))
 

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/principals.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/principals.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -87,8 +87,14 @@
     print("  --get-auto-schedule-mode: read auto-schedule mode")
     print("  --set-auto-accept-group=principal: set auto-accept-group")
     print("  --get-auto-accept-group: read auto-accept-group")
-    print("  --add {locations|resources} 'full name' [record name] [GUID]: add a principal")
+    print("  --add {locations|resources|addresses} 'full name' [record name] [GUID]: add a principal")
     print("  --remove: remove a principal")
+    print("  --set-geo=url: set the geo: url for an address (e.g. geo:37.331741,-122.030333)")
+    print("  --get-geo: get the geo: url for an address")
+    print("  --set-street-address=streetaddress: set the street address string for an address")
+    print("  --get-street-address: get the street address string for an address")
+    print("  --set-address=guid: associate principal with an address (by guid)")
+    print("  --get-address: get the associated address's guid")
 
     if e:
         sys.exit(64)
@@ -116,8 +122,29 @@
             directory = rootResource.getDirectory()
             yield self.function(rootResource, directory, self.store, *self.params)
 
+attrMap = {
+    'GeneratedUID' : { 'attr' : 'guid', },
+    'RealName' : { 'attr' : 'fullName', },
+    'RecordName' : { 'attr' : 'shortNames', },
+    'AutoSchedule' : { 'attr' : 'autoSchedule', },
+    'AutoAcceptGroup' : { 'attr' : 'autoAcceptGroup', },
 
+    'Comment' : { 'extras' : True, 'attr' : 'comment', },
+    'Description' : { 'extras' : True, 'attr' : 'description', },
+    'Type' : { 'extras' : True, 'attr' : 'type', },
 
+    # For "Locations", i.e. scheduled spaces
+    'Capacity' : { 'extras' : True, 'attr' : 'capacity', },
+    'Floor' : { 'extras' : True, 'attr' : 'floor', },
+    'AssociatedAddress' : { 'extras' : True, 'attr' : 'associatedAddress', },
+
+    # For "Addresses", i.e. nonscheduled areas containing Locations
+    'AbbreviatedName' : { 'extras' : True, 'attr' : 'abbreviatedName', },
+    'StreetAddress' : { 'extras' : True, 'attr' : 'streetAddress', },
+    'Geo' : { 'extras' : True, 'attr' : 'geo', },
+}
+
+
 def main():
     try:
         (optargs, args) = getopt(
@@ -142,6 +169,12 @@
                 "get-auto-schedule-mode",
                 "set-auto-accept-group=",
                 "get-auto-accept-group",
+                "set-geo=",
+                "get-geo",
+                "set-address=",
+                "get-address",
+                "set-street-address=",
+                "get-street-address",
                 "verbose",
             ],
         )
@@ -258,6 +291,24 @@
         elif opt in ("", "--get-auto-accept-group"):
             principalActions.append((action_getAutoAcceptGroup,))
 
+        elif opt in ("", "--set-geo"):
+            principalActions.append((action_setValue, "Geo", arg))
+
+        elif opt in ("", "--get-geo"):
+            principalActions.append((action_getValue, "Geo"))
+
+        elif opt in ("", "--set-street-address"):
+            principalActions.append((action_setValue, "StreetAddress", arg))
+
+        elif opt in ("", "--get-street-address"):
+            principalActions.append((action_getValue, "StreetAddress"))
+
+        elif opt in ("", "--set-address"):
+            principalActions.append((action_setValue, "AssociatedAddress", arg))
+
+        elif opt in ("", "--get-address"):
+            principalActions.append((action_getValue, "AssociatedAddress"))
+
         else:
             raise NotImplementedError(opt)
 
@@ -274,7 +325,7 @@
     elif addType:
 
         try:
-            addType = matchStrings(addType, ["locations", "resources"])
+            addType = matchStrings(addType, ["locations", "resources", "addresses"])
         except ValueError, e:
             print(e)
             return
@@ -296,7 +347,7 @@
     elif listPrincipals:
         try:
             listPrincipals = matchStrings(listPrincipals, ["users", "groups",
-                "locations", "resources"])
+                "locations", "resources", "addresses"])
         except ValueError, e:
             print(e)
             return
@@ -393,6 +444,7 @@
                  "groups" : "Group",
                  "locations" : "Place",
                  "resources" : "Resource",
+                 "addresses" : "Address",
                 }.get(record.recordType),
             ))
             print("   GUID: %s" % (record.guid,))
@@ -667,7 +719,30 @@
         print("No auto-accept-group assigned to %s" % (prettyPrincipal(principal),))
 
 
+ at inlineCallbacks
+def action_setValue(rootResource, directory, store, principal, name, value):
+    print("Setting %s to %s for %s" % (
+        name, value, prettyPrincipal(principal),
+    ))
 
+    principal.record.extras[attrMap[name]["attr"]] = value
+    (yield updateRecord(False, directory,
+        principal.record.recordType,
+        guid=principal.record.guid,
+        shortNames=principal.record.shortNames,
+        fullName=principal.record.fullName,
+        **principal.record.extras
+    ))
+
+
+def action_getValue(rootResource, directory, store, principal, name):
+    print("%s for %s is %s" % (
+        name,
+        prettyPrincipal(principal),
+        principal.record.extras[attrMap[name]["attr"]]
+    ))
+
+
 def abort(msg, status=1):
     sys.stdout.write("%s\n" % (msg,))
     try:

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/gateway/caldavd.plist
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/gateway/caldavd.plist	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/gateway/caldavd.plist	2014-01-06 22:03:14 UTC (rev 12243)
@@ -176,6 +176,7 @@
         <array>
             <string>resources</string>
             <string>locations</string>
+            <string>addresses</string>
         </array>
       </dict>
     </dict>

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/test_gateway.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/test_gateway.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/calendarserver/tools/test/test_gateway.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -17,7 +17,7 @@
 
 import os
 import sys
-from twext.python.plistlib import readPlistFromString
+from plistlib import readPlistFromString
 import xml
 
 from twext.python.filepath import CachingFilePath as FilePath
@@ -127,15 +127,9 @@
     def test_getLocationAttributes(self):
         yield self.runCommand(command_createLocation)
         results = yield self.runCommand(command_getLocationAttributes)
-        self.assertEquals(results["result"]["Building"], "Test Building")
-        self.assertEquals(results["result"]["City"], "Cupertino")
         self.assertEquals(results["result"]["Capacity"], "40")
         self.assertEquals(results["result"]["Description"], "Test Description")
-        self.assertEquals(results["result"]["ZIP"], "95014")
-        self.assertEquals(results["result"]["Floor"], "First")
         self.assertEquals(results["result"]["RecordName"], ["createdlocation01"])
-        self.assertEquals(results["result"]["State"], "CA")
-        self.assertEquals(results["result"]["Street"], "1 Infinite Loop")
         self.assertEquals(results["result"]["RealName"],
             "Created Location 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
         self.assertEquals(results["result"]["Comment"], "Test Comment")
@@ -162,6 +156,45 @@
 
 
     @inlineCallbacks
+    def test_createAddress(self):
+        directory = getDirectory()
+
+        record = directory.recordWithUID("C701069D-9CA1-4925-A1A9-5CD94767B74B")
+        self.assertEquals(record, None)
+        yield self.runCommand(command_createAddress)
+
+        directory.flushCaches()
+
+        record = directory.recordWithUID("C701069D-9CA1-4925-A1A9-5CD94767B74B")
+        self.assertEquals(record.fullName.decode("utf-8"),
+            "Created Address 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
+
+        self.assertNotEquals(record, None)
+
+        self.assertEquals(record.extras["abbreviatedName"], "Addr1")
+        self.assertEquals(record.extras["streetAddress"], "1 Infinite Loop\nCupertino, 95014\nCA")
+        self.assertEquals(record.extras["geo"], "geo:37.331,-122.030")
+
+        results = yield self.runCommand(command_getAddressList)
+        self.assertEquals(len(results["result"]), 1)
+
+        results = yield self.runCommand(command_getAddressAttributes)
+        self.assertEquals(results["result"]["RealName"], u'Created Address 01 \xd0 \U0001f4a3')
+
+        results = yield self.runCommand(command_setAddressAttributes)
+
+        results = yield self.runCommand(command_getAddressAttributes)
+        self.assertEquals(results["result"]["RealName"], u'Updated Address')
+        self.assertEquals(results["result"]["StreetAddress"], u'Updated Street Address')
+        self.assertEquals(results["result"]["Geo"], u'Updated Geo')
+        
+        results = yield self.runCommand(command_deleteAddress)
+
+        results = yield self.runCommand(command_getAddressList)
+        self.assertEquals(len(results["result"]), 0)
+
+
+    @inlineCallbacks
     def test_createLocation(self):
         directory = getDirectory()
 
@@ -184,15 +217,8 @@
         self.assertEquals(record.autoSchedule, True)
 
         self.assertEquals(record.extras["comment"], "Test Comment")
-        self.assertEquals(record.extras["building"], "Test Building")
         self.assertEquals(record.extras["floor"], "First")
         self.assertEquals(record.extras["capacity"], "40")
-        self.assertEquals(record.extras["street"], "1 Infinite Loop")
-        self.assertEquals(record.extras["city"], "Cupertino")
-        self.assertEquals(record.extras["state"], "CA")
-        self.assertEquals(record.extras["zip"], "95014")
-        self.assertEquals(record.extras["country"], "USA")
-        self.assertEquals(record.extras["phone"], "(408) 555-1212")
 
         results = yield self.runCommand(command_getLocationAttributes)
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03', 'user04']))
@@ -215,15 +241,9 @@
         record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
 
         self.assertEquals(record.extras["comment"], "Updated Test Comment")
-        self.assertEquals(record.extras["building"], "Updated Test Building")
         self.assertEquals(record.extras["floor"], "Second")
         self.assertEquals(record.extras["capacity"], "41")
-        self.assertEquals(record.extras["street"], "2 Infinite Loop")
-        self.assertEquals(record.extras["city"], "Updated Cupertino")
-        self.assertEquals(record.extras["state"], "Updated CA")
-        self.assertEquals(record.extras["zip"], "95015")
-        self.assertEquals(record.extras["country"], "Updated USA")
-        self.assertEquals(record.extras["phone"], "(408) 555-1213")
+        self.assertEquals(record.extras["streetAddress"], "2 Infinite Loop\nCupertino, 95014\nCA")
         self.assertEquals(record.autoSchedule, True)
         self.assertEquals(record.autoAcceptGroup, "F5A6142C-4189-4E9E-90B0-9CD0268B314B")
 
@@ -384,6 +404,31 @@
 </plist>
 """
 
+command_createAddress = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>createAddress</string>
+        <key>GeneratedUID</key>
+        <string>C701069D-9CA1-4925-A1A9-5CD94767B74B</string>
+        <key>RealName</key>
+        <string>Created Address 01 %s %s</string>
+        <key>AbbreviatedName</key>
+        <string>Addr1</string>
+        <key>RecordName</key>
+        <array>
+                <string>createdaddress01</string>
+        </array>
+        <key>StreetAddress</key>
+        <string>1 Infinite Loop\nCupertino, 95014\nCA</string>
+        <key>Geo</key>
+        <string>geo:37.331,-122.030</string>
+</dict>
+</plist>
+""" % (unichr(208), u"\ud83d\udca3")
+
+
 command_createLocation = """<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
@@ -406,24 +451,12 @@
         <string>Test Comment</string>
         <key>Description</key>
         <string>Test Description</string>
-        <key>Building</key>
-        <string>Test Building</string>
         <key>Floor</key>
         <string>First</string>
         <key>Capacity</key>
         <string>40</string>
-        <key>Street</key>
-        <string>1 Infinite Loop</string>
-        <key>City</key>
-        <string>Cupertino</string>
-        <key>State</key>
-        <string>CA</string>
-        <key>ZIP</key>
-        <string>95014</string>
-        <key>Country</key>
-        <string>USA</string>
-        <key>Phone</key>
-        <string>(408) 555-1212</string>
+        <key>AssociatedAddress</key>
+        <string>C701069D-9CA1-4925-A1A9-5CD94767B74B</string>
         <key>ReadProxies</key>
         <array>
             <string>users:user03</string>
@@ -451,14 +484,16 @@
         <string>AF575A61-CFA6-49E1-A0F6-B5662C9D9801</string>
         <key>RealName</key>
         <string>Laptop 1</string>
+        <key>Comment</key>
+        <string>Test Comment</string>
+        <key>Description</key>
+        <string>Test Description</string>
         <key>Type</key>
         <string>Computer</string>
         <key>RecordName</key>
         <array>
                 <string>laptop1</string>
         </array>
-        <key>Comment</key>
-        <string>Test Comment</string>
         <key>ReadProxies</key>
         <array>
             <string>users:user03</string>
@@ -497,6 +532,19 @@
 </plist>
 """
 
+
+command_deleteAddress = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>deleteAddress</string>
+        <key>GeneratedUID</key>
+        <string>C701069D-9CA1-4925-A1A9-5CD94767B74B</string>
+</dict>
+</plist>
+"""
+
 command_getLocationAndResourceList = """<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
@@ -527,6 +575,17 @@
 </plist>
 """
 
+command_getAddressList = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>getAddressList</string>
+</dict>
+</plist>
+"""
+
+
 command_listReadProxies = """<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
@@ -601,24 +660,12 @@
         <string>Updated Test Comment</string>
         <key>Description</key>
         <string>Updated Test Description</string>
-        <key>Building</key>
-        <string>Updated Test Building</string>
         <key>Floor</key>
         <string>Second</string>
         <key>Capacity</key>
         <string>41</string>
-        <key>Street</key>
-        <string>2 Infinite Loop</string>
-        <key>City</key>
-        <string>Updated Cupertino</string>
-        <key>State</key>
-        <string>Updated CA</string>
-        <key>ZIP</key>
-        <string>95015</string>
-        <key>Country</key>
-        <string>Updated USA</string>
-        <key>Phone</key>
-        <string>(408) 555-1213</string>
+        <key>StreetAddress</key>
+        <string>2 Infinite Loop\nCupertino, 95014\nCA</string>
         <key>ReadProxies</key>
         <array>
             <string>users:user03</string>
@@ -645,6 +692,38 @@
 </plist>
 """
 
+command_getAddressAttributes = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>getAddressAttributes</string>
+        <key>GeneratedUID</key>
+        <string>C701069D-9CA1-4925-A1A9-5CD94767B74B</string>
+</dict>
+</plist>
+"""
+
+command_setAddressAttributes = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>setAddressAttributes</string>
+        <key>GeneratedUID</key>
+        <string>C701069D-9CA1-4925-A1A9-5CD94767B74B</string>
+        <key>RealName</key>
+        <string>Updated Address</string>
+        <key>StreetAddress</key>
+        <string>Updated Street Address</string>
+        <key>Geo</key>
+        <string>Updated Geo</string>
+
+</dict>
+</plist>
+"""
+
+
 command_setResourceAttributes = """<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/augments-test.xml	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/augments-test.xml	2014-01-06 22:03:14 UTC (rev 12243)
@@ -109,6 +109,7 @@
     <enable-addressbook>true</enable-addressbook>
     <enable-login>true</enable-login>
     <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
   </record>
   <record>
     <uid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</uid>
@@ -136,4 +137,49 @@
     <enable-login>true</enable-login>
     <auto-schedule>true</auto-schedule>
   </record>
+  <record>
+    <uid>6F9EE33B-78F6-481B-9289-3D0812FF0D64</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>false</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>false</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>false</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>06E3BDCB-9C19-485A-B14E-F146A80ADDC6</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>4D66A20A-1437-437D-8069-2F14E8322234</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
 </augments>

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/resources-test.xml
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/resources-test.xml	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/conf/auth/resources-test.xml	2014-01-06 22:03:14 UTC (rev 12243)
@@ -1,5 +1,13 @@
 <accounts realm="Test Realm">
   <location>
+    <uid>fantastic</uid>
+    <guid>4D66A20A-1437-437D-8069-2F14E8322234</guid>
+    <name>Fantastic Conference Room</name>
+    <extras>
+      <associatedAddress>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</associatedAddress>
+    </extras>
+  </location>
+  <location>
     <uid>jupiter</uid>
     <guid>jupiter</guid>
     <name>Jupiter Conference Room, Building 2, 1st Floor</name>
@@ -78,6 +86,9 @@
     <uid>sharissroom</uid>
     <guid>80689D41-DAF8-4189-909C-DB017B271892</guid>
     <name>Shari's Room</name>
+    <extras>
+      <associatedAddress>6F9EE33B-78F6-481B-9289-3D0812FF0D64</associatedAddress>
+    </extras>
   </location>
   <location>
     <uid>pluto</uid>
@@ -95,6 +106,14 @@
     <name>Room 10</name>
   </location>
   <location>
+    <uid>pretend</uid>
+    <guid>06E3BDCB-9C19-485A-B14E-F146A80ADDC6</guid>
+    <name>Pretend Conference Room</name>
+    <extras>
+      <associatedAddress>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</associatedAddress>
+    </extras>
+  </location>
+  <location>
     <uid>neptune</uid>
     <guid>neptune</guid>
     <name>Neptune Conference Room, Building 2, 1st Floor</name>
@@ -224,4 +243,31 @@
     <guid>resource09</guid>
     <name>Resource 09</name>
   </resource>
+  <address>
+    <uid>testaddress1</uid>
+    <guid>6F9EE33B-78F6-481B-9289-3D0812FF0D64</guid>
+    <name>Test Address One</name>
+    <extras>
+      <streetAddress>20300 Stevens Creek Blvd, Cupertino, CA 95014</streetAddress>
+      <geo>37.322281,-122.028345</geo>
+    </extras>
+  </address>
+  <address>
+    <uid>il2</uid>
+    <guid>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</guid>
+    <name>IL2</name>
+    <extras>
+      <streetAddress>2 Infinite Loop, Cupertino, CA 95014</streetAddress>
+      <geo>37.332633,-122.030502</geo>
+    </extras>
+  </address>
+  <address>
+    <uid>il1</uid>
+    <guid>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</guid>
+    <name>IL1</name>
+    <extras>
+      <streetAddress>1 Infinite Loop, Cupertino, CA 95014</streetAddress>
+      <geo>37.331741,-122.030333</geo>
+    </extras>
+  </address>
 </accounts>

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/conf/caldavd-test.plist	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/conf/caldavd-test.plist	2014-01-06 22:03:14 UTC (rev 12243)
@@ -284,29 +284,20 @@
                     <string>mail</string>
                     <string>mailAlias</string>
                 </array>
-                <key>firstName</key>
-                <string></string>
-                <key>lastName</key>
-                <string></string>
             </dict>
           </dict>
           <key>locations</key>
           <dict>
             <key>rdn</key>
             <string>ou=locations</string>
+            <key>associatedAddressAttr</key>
+            <string></string>
             <key>mapping</key>
             <dict>
                 <key>recordName</key>
                 <string>cn</string>
                 <key>fullName</key>
                 <string>cn</string>
-                <key>emailAddresses</key>
-                <array>
-                </array>
-                <key>firstName</key>
-                <string></string>
-                <key>lastName</key>
-                <string></string>
             </dict>
           </dict>
           <key>resources</key>
@@ -319,15 +310,24 @@
                 <string>cn</string>
                 <key>fullName</key>
                 <string>cn</string>
-                <key>emailAddresses</key>
-                <array>
-                </array>
-                <key>firstName</key>
-                <string></string>
-                <key>lastName</key>
-                <string></string>
             </dict>
           </dict>
+          <key>addresses</key>
+          <dict>
+            <key>rdn</key>
+            <string>ou=buildings</string>
+            <key>geoAttr</key>
+            <string></string>
+            <key>streetAddressAttr</key>
+            <string></string>
+            <key>mapping</key>
+            <dict>
+                <key>recordName</key>
+                <string>cn</string>
+                <key>fullName</key>
+                <string>cn</string>
+            </dict>
+          </dict>
         </dict>
         <key>groupSchema</key>
         <dict>

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/datafilters/peruserdata.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/datafilters/peruserdata.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/datafilters/peruserdata.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -67,7 +67,7 @@
 
     # X- properties that are ignored - by default all X- properties are treated as per-user except for the
     # ones listed here
-    IGNORE_X_PROPERTIES = (Component.HIDDEN_INSTANCE_PROPERTY,)
+    IGNORE_X_PROPERTIES = [Component.HIDDEN_INSTANCE_PROPERTY]
 
     def __init__(self, uid):
         """

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/augment.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/augment.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/augment.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -81,6 +81,7 @@
     "groups" : "Group",
     "locations" : "Location",
     "resources" : "Resource",
+    "addresses" : "Address",
 }
 
 class AugmentDB(object):

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/directory.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/directory.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -86,6 +86,7 @@
     recordType_groups = "groups"
     recordType_locations = "locations"
     recordType_resources = "resources"
+    recordType_addresses = "addresses"
 
     searchContext_location = "location"
     searchContext_resource = "resource"

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/ldapdirectory.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/ldapdirectory.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -1,6 +1,6 @@
 ##
 # Copyright (c) 2008-2009 Aymeric Augustin. All rights reserved.
-# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2014 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.
@@ -112,8 +112,6 @@
                 "guidAttr": "entryUUID",
                 "users": {
                     "rdn": "ou=People",
-                    "attr": "uid", # used only to synthesize email address
-                    "emailSuffix": None, # used only to synthesize email address
                     "filter": None, # additional filter for this type
                     "loginEnabledAttr" : "", # attribute controlling login
                     "loginEnabledValue" : "yes", # "True" value of above attribute
@@ -129,8 +127,6 @@
                 },
                 "groups": {
                     "rdn": "ou=Group",
-                    "attr": "cn", # used only to synthesize email address
-                    "emailSuffix": None, # used only to synthesize email address
                     "filter": None, # additional filter for this type
                     "mapping" : { # maps internal record names to LDAP
                         "recordName": "cn",
@@ -142,23 +138,18 @@
                 },
                 "locations": {
                     "rdn": "ou=Places",
-                    "attr": "cn", # used only to synthesize email address
-                    "emailSuffix": None, # used only to synthesize email address
                     "filter": None, # additional filter for this type
                     "calendarEnabledAttr" : "", # attribute controlling enabledForCalendaring
                     "calendarEnabledValue" : "yes", # "True" value of above attribute
+                    "associatedAddressAttr" : "",
                     "mapping" : { # maps internal record names to LDAP
                         "recordName": "cn",
                         "fullName" : "cn",
                         "emailAddresses" : ["mail"], # multiple LDAP fields supported
-                        "firstName" : "givenName",
-                        "lastName" : "sn",
                     },
                 },
                 "resources": {
                     "rdn": "ou=Resources",
-                    "attr": "cn", # used only to synthesize email address
-                    "emailSuffix": None, # used only to synthesize email address
                     "filter": None, # additional filter for this type
                     "calendarEnabledAttr" : "", # attribute controlling enabledForCalendaring
                     "calendarEnabledValue" : "yes", # "True" value of above attribute
@@ -166,10 +157,18 @@
                         "recordName": "cn",
                         "fullName" : "cn",
                         "emailAddresses" : ["mail"], # multiple LDAP fields supported
-                        "firstName" : "givenName",
-                        "lastName" : "sn",
                     },
                 },
+                "addresses": {
+                    "rdn": "ou=Buildings",
+                    "filter": None, # additional filter for this type
+                    "streetAddressAttr" : "",
+                    "geoAttr" : "",
+                    "mapping" : { # maps internal record names to LDAP
+                        "recordName": "cn",
+                        "fullName" : "cn",
+                    },
+                },
             },
             "groupSchema": {
                 "membersAttr": "member", # how members are specified
@@ -238,8 +237,10 @@
         for recordType in self.recordTypes():
             if self.rdnSchema[recordType]["attr"]:
                 attrSet.add(self.rdnSchema[recordType]["attr"])
-            if self.rdnSchema[recordType].get("calendarEnabledAttr", False):
-                attrSet.add(self.rdnSchema[recordType]["calendarEnabledAttr"])
+            for n in ("calendarEnabledAttr", "associatedAddressAttr",
+                "streetAddressAttr", "geoAttr"):
+                if self.rdnSchema[recordType].get(n, False):
+                    attrSet.add(self.rdnSchema[recordType][n])
             for attrList in self.rdnSchema[recordType]["mapping"].values():
                 if attrList:
                     # Since emailAddresses can map to multiple LDAP fields,
@@ -308,7 +309,7 @@
 
         # Build filter
         filterstr = "(!(objectClass=organizationalUnit))"
-        typeFilter = self.rdnSchema[recordType]["filter"]
+        typeFilter = self.rdnSchema[recordType].get("filter", "")
         if typeFilter:
             filterstr = "(&%s%s)" % (filterstr, typeFilter)
 
@@ -779,6 +780,7 @@
         enabledForAddressBooks = None
         uid = None
         enabledForLogin = True
+        extras = {}
 
         shortNames = tuple(self._getMultipleLdapAttributes(attrs, self.rdnSchema[recordType]["mapping"]["recordName"]))
         if not shortNames:
@@ -795,17 +797,17 @@
 
         # Find or build email
         # (The emailAddresses mapping is a list of ldap fields)
-        emailAddressesMappedTo = self.rdnSchema[recordType]["mapping"]["emailAddresses"]
+        emailAddressesMappedTo = self.rdnSchema[recordType]["mapping"].get("emailAddresses", "")
         # Supporting either string or list for emailAddresses:
         if isinstance(emailAddressesMappedTo, str):
-            emailAddresses = set(self._getMultipleLdapAttributes(attrs, self.rdnSchema[recordType]["mapping"]["emailAddresses"]))
+            emailAddresses = set(self._getMultipleLdapAttributes(attrs, self.rdnSchema[recordType]["mapping"].get("emailAddresses", "")))
         else:
             emailAddresses = set(self._getMultipleLdapAttributes(attrs, *self.rdnSchema[recordType]["mapping"]["emailAddresses"]))
-        emailSuffix = self.rdnSchema[recordType]["emailSuffix"]
+        emailSuffix = self.rdnSchema[recordType].get("emailSuffix", None)
 
         if len(emailAddresses) == 0 and emailSuffix:
             emailPrefix = self._getUniqueLdapAttribute(attrs,
-                self.rdnSchema[recordType]["attr"])
+                self.rdnSchema[recordType].get("attr", "cn"))
             emailAddresses.add(emailPrefix + emailSuffix)
 
         proxyGUIDs = ()
@@ -891,6 +893,25 @@
                     autoAcceptGroup = self._getUniqueLdapAttribute(attrs,
                         self.resourceSchema["autoAcceptGroupAttr"])
 
+            if recordType == self.recordType_locations:
+                if self.rdnSchema[recordType]["associatedAddressAttr"]:
+                    associatedAddress = self._getUniqueLdapAttribute(attrs,
+                        self.rdnSchema[recordType]["associatedAddressAttr"])
+                    if associatedAddress:
+                        extras["associatedAddress"] = associatedAddress
+
+        elif recordType == self.recordType_addresses:
+            if self.rdnSchema[recordType].get("geoAttr", ""):
+                geo = self._getUniqueLdapAttribute(attrs,
+                    self.rdnSchema[recordType]["geoAttr"])
+                if geo:
+                    extras["geo"] = geo
+            if self.rdnSchema[recordType].get("streetAddressAttr", ""):
+                street = self._getUniqueLdapAttribute(attrs,
+                    self.rdnSchema[recordType]["streetAddressAttr"])
+                if street:
+                    extras["streetAddress"] = street
+
         serverID = partitionID = None
         if self.partitionSchema["serverIdAttr"]:
             serverID = self._getUniqueLdapAttribute(attrs,
@@ -915,6 +936,7 @@
             extProxies              = proxyGUIDs,
             extReadOnlyProxies      = readOnlyProxyGUIDs,
             attrs                   = attrs,
+            **extras
         )
 
         if self.augmentService is not None:
@@ -986,7 +1008,7 @@
 
             # Build filter
             filterstr = "(!(objectClass=organizationalUnit))"
-            typeFilter = self.rdnSchema[recordType]["filter"]
+            typeFilter = self.rdnSchema[recordType].get("filter", "")
             if typeFilter:
                 filterstr = "(&%s%s)" % (filterstr, typeFilter)
 
@@ -1007,17 +1029,17 @@
             elif indexType == self.INDEX_TYPE_CUA:
                 # indexKey is of the form "mailto:test at example.net"
                 email = indexKey[7:] # strip "mailto:"
-                emailSuffix = self.rdnSchema[recordType]["emailSuffix"]
+                emailSuffix = self.rdnSchema[recordType].get("emailSuffix", None)
                 if emailSuffix is not None and email.partition("@")[2] == emailSuffix:
                     filterstr = "(&%s(|(&(!(mail=*))(%s=%s))(mail=%s)))" % (
                         filterstr,
-                        self.rdnSchema[recordType]["attr"],
+                        self.rdnSchema[recordType].get("attr", "cn"),
                         email.partition("@")[0],
                         ldapEsc(email)
                     )
                 else:
                     # emailAddresses can map to multiple LDAP fields
-                    ldapFields = self.rdnSchema[recordType]["mapping"]["emailAddresses"]
+                    ldapFields = self.rdnSchema[recordType]["mapping"].get("emailAddresses", "")
                     if isinstance(ldapFields, str):
                         if ldapFields:
                             subfilter = "(%s=%s)" % (ldapFields, ldapEsc(email))
@@ -1112,7 +1134,7 @@
             typeCounts[recordType] = 0
             base = self.typeDNs[recordType]
             scope = ldap.SCOPE_SUBTREE
-            extraFilter = self.rdnSchema[recordType]["filter"]
+            extraFilter = self.rdnSchema[recordType].get("filter", "")
             filterstr = buildFilterFromTokens(recordType, self.rdnSchema[recordType]["mapping"],
                 tokens, extra=extraFilter)
 
@@ -1453,7 +1475,7 @@
     """
 
     filterStr = None
-    tokens = [ldapEsc(t) for t in tokens if len(t) > 2]
+    tokens = [ldapEsc(t) for t in tokens]
     if len(tokens) == 0:
         return None
 
@@ -1506,7 +1528,7 @@
         guid, shortNames, authIDs, fullName,
         firstName, lastName, emailAddresses,
         uid, dn, memberGUIDs, extProxies, extReadOnlyProxies,
-        attrs
+        attrs, **kwargs
     ):
         super(LdapDirectoryRecord, self).__init__(
             service               = service,
@@ -1521,6 +1543,7 @@
             extProxies            = extProxies,
             extReadOnlyProxies    = extReadOnlyProxies,
             uid                   = uid,
+            **kwargs
         )
 
         # Save attributes of dn and attrs in case you might need them later

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_aggregate.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_aggregate.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_aggregate.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -55,6 +55,7 @@
     groups = property(_records("groups"))
     locations = property(_records("locations"))
     resources = property(_records("resources"))
+    addresses = property(_records("addresses"))
 
     recordTypePrefixes = tuple(s[0] for s in testServices)
 

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_ldapdirectory.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_ldapdirectory.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_ldapdirectory.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -540,8 +540,6 @@
                             "recordName": "cn",
                             "fullName" : "cn",
                             "emailAddresses" : ["mail", "emailAliases"],
-                            "firstName" : "givenName",
-                            "lastName" : "sn",
                         },
                     },
                     "locations": {
@@ -551,12 +549,11 @@
                         "filter": "(objectClass=apple-resource)", # additional filter for this type
                         "calendarEnabledAttr" : "", # attribute controlling calendaring
                         "calendarEnabledValue" : "yes", # "True" value of above attribute
+                        "associatedAddressAttr" : "assocAddr",
                         "mapping": { # maps internal record names to LDAP
                             "recordName": "cn",
                             "fullName" : "cn",
                             "emailAddresses" : "", # old style, single string
-                            "firstName" : "givenName",
-                            "lastName" : "sn",
                         },
                     },
                     "resources": {
@@ -570,10 +567,17 @@
                             "recordName": "cn",
                             "fullName" : "cn",
                             "emailAddresses" : [], # new style, array
-                            "firstName" : "givenName",
-                            "lastName" : "sn",
                         },
                     },
+                    "addresses": {
+                        "rdn": "cn=Buildings",
+                        "geoAttr" : "coordinates",
+                        "streetAddressAttr" : "postal",
+                        "mapping": { # maps internal record names to LDAP
+                            "recordName": "cn",
+                            "fullName" : "cn",
+                        },
+                    },
                 },
                 "groupSchema": {
                     "membersAttr": "uniqueMember", # how members are specified
@@ -1521,6 +1525,39 @@
                 self.service.recordType_users)
             self.assertEquals(record.guid, guid.upper())
 
+            # Location with associated Address
+
+            dn = "cn=odtestlocation,cn=locations,dc=example,dc=com"
+            guid = "D3094652-344B-4633-8DB8-09639FA00FB6"
+            attrs = {
+                "apple-generateduid": [guid],
+                "cn": ["odtestlocation"],
+                "assocAddr" : ["6C6CD280-E6E3-11DF-9492-0800200C9A66"],
+            }
+            record = self.service._ldapResultToRecord(dn, attrs,
+                self.service.recordType_locations)
+            self.assertEquals(record.extras, {
+                "associatedAddress": "6C6CD280-E6E3-11DF-9492-0800200C9A66"
+            })
+           
+            # Address with street and geo
+
+            dn = "cn=odtestaddress,cn=buildings,dc=example,dc=com"
+            guid = "6C6CD280-E6E3-11DF-9492-0800200C9A66"
+            attrs = {
+                "apple-generateduid": [guid],
+                "cn": ["odtestaddress"],
+                "coordinates" : ["geo:1,2"],
+                "postal" : ["1 Infinite Loop, Cupertino, CA"],
+            }
+            record = self.service._ldapResultToRecord(dn, attrs,
+                self.service.recordType_addresses)
+            self.assertEquals(record.extras, {
+                "geo": "geo:1,2",
+                "streetAddress" : "1 Infinite Loop, Cupertino, CA",
+            })
+           
+
         def test_listRecords(self):
             """
             listRecords makes an LDAP query (with fake results in this test)

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_xmlfile.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/test_xmlfile.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -34,7 +34,8 @@
         DirectoryService.recordType_users,
         DirectoryService.recordType_groups,
         DirectoryService.recordType_locations,
-        DirectoryService.recordType_resources
+        DirectoryService.recordType_resources,
+        DirectoryService.recordType_addresses,
     ))
 
     users = {

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/util.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/util.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/test/util.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -44,6 +44,10 @@
     # Subclass should init this to a dict of resourcenames keys and dict values.
     resources = {}
 
+    # Subclass should init this to a dict of addressname keys and dict values.
+    addresses = {}
+
+
     # Subclass should init this to an IDirectoryService implementation class.
     def service(self):
         """

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlaccountsparser.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlaccountsparser.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -41,6 +41,7 @@
 ELEMENT_GROUP             = "group"
 ELEMENT_LOCATION          = "location"
 ELEMENT_RESOURCE          = "resource"
+ELEMENT_ADDRESS           = "address"
 
 ELEMENT_SHORTNAME         = "uid"
 ELEMENT_GUID              = "guid"
@@ -65,6 +66,7 @@
     ELEMENT_GROUP    : DirectoryService.recordType_groups,
     ELEMENT_LOCATION : DirectoryService.recordType_locations,
     ELEMENT_RESOURCE : DirectoryService.recordType_resources,
+    ELEMENT_ADDRESS  : DirectoryService.recordType_addresses,
 }
 
 class XMLAccountsParser(object):

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlfile.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/directory/xmlfile.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -71,6 +71,7 @@
                 self.recordType_groups,
                 self.recordType_locations,
                 self.recordType_resources,
+                self.recordType_addresses,
             ),
             'realmName' : '/Search',
             'statSeconds' : 15,
@@ -373,6 +374,7 @@
             'groups'    : 'group',
             'locations' : 'location',
             'resources' : 'resource',
+            'addresses' : 'address',
         }
         xmlType = xmlTypes[principal.recordType]
 

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/stdconfig.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/twistedcaldav/stdconfig.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -34,6 +34,7 @@
 from twistedcaldav.util import getPasswordFromKeychain
 from twistedcaldav.util import KeychainAccessError, KeychainPasswordNotFound
 from twistedcaldav.util import computeProcessCount
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 
 from calendarserver.push.util import getAPNTopicFromCertificate
 
@@ -166,7 +167,7 @@
 DEFAULT_RESOURCE_PARAMS = {
     "twistedcaldav.directory.xmlfile.XMLDirectoryService": {
         "xmlFile": "resources.xml",
-        "recordTypes" : ("locations", "resources"),
+        "recordTypes" : ("locations", "resources", "addresses"),
     },
     "twistedcaldav.directory.appleopendirectory.OpenDirectoryService": {
         "node": "/Search",
@@ -583,10 +584,13 @@
 
         "Calendars" : {
             "Enabled"         : True, # Calendar on/off switch
+            "IgnorePerUserProperties" : [
+                "X-APPLE-STRUCTURED-LOCATION",
+            ],
         },
         "AddressBooks" : {
             "Enabled"         : False, # Address Books on/off switch
-        }
+        },
     },
 
     "RestrictCalendarsToOneComponentType" : True, # Only allow calendars to be created with a single component type
@@ -1516,7 +1520,16 @@
                         (direction,))
 
 
+def _updateSharing(configDict, reloading=False):
+    #
+    # Sharing
+    #
 
+    # Transfer configured non-per-user property names to PerUserDataFilter
+    for propertyName in configDict.Sharing.Calendars.IgnorePerUserProperties:
+        PerUserDataFilter.IGNORE_X_PROPERTIES.append(propertyName)
+
+
 def _updateServers(configDict, reloading=False):
     from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
     if configDict.Servers.Enabled:
@@ -1594,6 +1607,7 @@
     _updateLogLevels,
     _updateNotifications,
     _updateScheduling,
+    _updateSharing,
     _updateServers,
     _updateCompliance,
     )

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/sql.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/sql.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -1938,6 +1938,43 @@
             self._componentChanged = True
 
 
+    def addStructuredLocation(self, component):
+        """
+        Scan the component for ROOM attendees; if any are associated with an
+        address record which has street address and geo coordinates, add an
+        X-APPLE-STRUCTURED-LOCATION property and update the LOCATION property
+        to contain the name and street address.
+        """
+        for sub in component.subcomponents():
+            for attendee in sub.getAllAttendeeProperties():
+                if attendee.parameterValue("CUTYPE") == "ROOM":
+                    value = attendee.value()
+                    if value.startswith("urn:uuid:"):
+                        guid = value[9:]
+                        loc = self.directoryService().recordWithGUID(guid)
+                        if loc is not None:
+                            guid = loc.extras.get("associatedAddress",
+                                None)
+                            if guid is not None:
+                                addr = self.directoryService().recordWithGUID(guid)
+                                if addr is not None:
+                                    street = addr.extras.get("streetAddress", "")
+                                    geo = addr.extras.get("geo", "")
+                                    if street and geo:
+                                        title = attendee.parameterValue("CN")
+                                        params = {
+                                            "X-ADDRESS" : street,
+                                            "X-APPLE-RADIUS" : "71",
+                                            "X-TITLE" : title,
+                                        }
+                                        structured = Property("X-APPLE-STRUCTURED-LOCATION",
+                                            "geo:%s" % (geo,), params=params,
+                                            valuetype=PyCalendarValue.VALUETYPE_URI)
+                                        sub.replaceProperty(structured)
+                                        sub.replaceProperty(Property("LOCATION",
+                                            "%s\n%s" % (title, street)))
+
+
     @inlineCallbacks
     def doImplicitScheduling(self, component, inserting, internal_state, split_details=None):
 
@@ -2150,6 +2187,9 @@
             # Default/duplicate alarms
             self.processAlarms(component, inserting)
 
+            # Process structured location
+            self.addStructuredLocation(component)
+
             # Do scheduling
             implicit_result = (yield self.doImplicitScheduling(component, inserting, internal_state))
             if isinstance(implicit_result, int):

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/test_sql.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/test_sql.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -2134,7 +2134,82 @@
         self.assertEqual(len(self.flushLoggedErrors(InvalidOverriddenInstanceError)), 1)
 
 
+    @inlineCallbacks
+    def test_setComponent_structuredLocation(self):
+        """
+        Verify ROOM attendees who have street address and geo information
+        within the directory will get X-APPLE-STRUCTURED-LOCATION properties
+        added, as well as updated LOCATION properties.
+        """
 
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//Mac OS X 10.9.1//EN
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+DTSTART;TZID=America/Los_Angeles:20131211T164500
+DTEND;TZID=America/Los_Angeles:20131211T174500
+ATTENDEE;CN=Conference Room One;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
+ T;SCHEDULE-STATUS=2.0:urn:uuid:room1
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user01
+CREATED:20131211T221854Z
+DTSTAMP:20131211T230632Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RRULE:FREQ=DAILY;COUNT=5
+SEQUENCE:8
+SUMMARY:locations
+TRANSP:OPAQUE
+END:VEVENT
+BEGIN:VEVENT
+UID:561F5DBB-3F38-4B3A-986F-DD05CBAF554F
+RECURRENCE-ID;TZID=America/Los_Angeles:20131214T164500
+DTSTART;TZID=America/Los_Angeles:20131214T160000
+DTEND;TZID=America/Los_Angeles:20131214T170000
+ATTENDEE;CN=Conference Room Two;CUTYPE=ROOM;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPAN
+ T;SCHEDULE-STATUS=2.0:urn:uuid:room2
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user01
+CREATED:20131211T221854Z
+DTSTAMP:20131211T230632Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:8
+SUMMARY:locations
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+        yield calendar.createCalendarObjectWithName("structured.ics",
+            Component.fromString(data))
+        cobj = yield self.calendarObjectUnderTest(name="structured.ics",
+            calendar_name="calendar", home="user01")
+        comp = yield cobj.component()
+        components = list(comp.subcomponents())
+
+        # Check first component
+        locProp = components[0].getProperty("LOCATION")
+        self.assertEquals(locProp.value(),
+            "Conference Room One\n1 Infinite Loop, Cupertino, CA 95014")
+        structProp = components[0].getProperty("X-APPLE-STRUCTURED-LOCATION")
+        self.assertEquals(structProp.value(),
+            "geo:37.331741,-122.030333")
+
+        # Check second component
+        locProp = components[1].getProperty("LOCATION")
+        self.assertEquals(locProp.value(),
+            "Conference Room Two\n2 Infinite Loop, Cupertino, CA 95014")
+        structProp = components[1].getProperty("X-APPLE-STRUCTURED-LOCATION")
+        self.assertEquals(structProp.value(),
+            "geo:37.332633,-122.030502")
+
+        yield self.commit()
+
+
+
+
 class CalendarObjectSplitting(CommonCommonTests, unittest.TestCase):
     """
     CalendarObject splitting tests

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/util.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/caldav/datastore/test/util.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -63,9 +63,11 @@
         cutype="INDIVIDUAL",
         locallyHosted=True,
         thisServer=True,
+        extras={},
     ):
 
-        super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName)
+        super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames,
+            fullName, extras=extras)
         self.uid = uid
         self.shortNames = shortNames
         self.fullName = fullName
@@ -164,6 +166,36 @@
     for uid in homes:
         directory.addRecord(buildDirectoryRecord(uid))
 
+    # Structured Locations
+    directory.addRecord(TestCalendarStoreDirectoryRecord(
+        "il1", ("il1",), "1 Infinite Loop", [],
+        extras={
+            "geo" : "37.331741,-122.030333",
+            "streetAddress" : "1 Infinite Loop, Cupertino, CA 95014",
+        }
+    ))
+    directory.addRecord(TestCalendarStoreDirectoryRecord(
+        "il2", ("il2",), "2 Infinite Loop", [],
+        extras={
+            "geo" : "37.332633,-122.030502",
+            "streetAddress" : "2 Infinite Loop, Cupertino, CA 95014",
+        }
+    ))
+    directory.addRecord(TestCalendarStoreDirectoryRecord(
+        "room1", ("room1",), "Conference Room One",
+        frozenset(("urn:uuid:room1",)),
+        extras={
+            "associatedAddress" : "il1",
+        }
+    ))
+    directory.addRecord(TestCalendarStoreDirectoryRecord(
+        "room2", ("room2",), "Conference Room Two",
+        frozenset(("urn:uuid:room2",)),
+        extras={
+            "associatedAddress" : "il2",
+        }
+    ))
+
     return directory
 
 

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/datastore/test/util.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/datastore/test/util.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -111,6 +111,11 @@
     def recordWithUID(self, uid):
         return self.records.get(uid)
 
+    def recordWithGUID(self, guid):
+        for record in self.records.itervalues():
+            if record.guid == guid:
+                return record
+        return None
 
     def addRecord(self, record):
         self.records[record.uid] = record
@@ -121,11 +126,13 @@
 
     implements(IStoreDirectoryRecord)
 
-    def __init__(self, uid, shortNames, fullName):
+    def __init__(self, uid, shortNames, fullName, extras={}):
         self.uid = uid
+        self.guid = uid
         self.shortNames = shortNames
         self.fullName = fullName
         self.displayName = self.fullName if self.fullName else self.shortNames[0]
+        self.extras = extras
 
 
 

Modified: CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/idirectoryservice.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/idirectoryservice.py	2014-01-06 21:46:00 UTC (rev 12242)
+++ CalendarServer/branches/release/CalendarServer-5.2-dev/txdav/common/idirectoryservice.py	2014-01-06 22:03:14 UTC (rev 12243)
@@ -39,8 +39,15 @@
         @rtype: L{IStoreDirectoryRecord}
         """
 
+    def recordWithGUID(guid): #@NoSelf
+        """
+        Return the record for the specified store guid.
 
+        @return: the record.
+        @rtype: L{IStoreDirectoryRecord}
+        """
 
+
 class IStoreDirectoryRecord(Interface):
     """
     Directory record object
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/5f7ecd8b/attachment.html>


More information about the calendarserver-changes mailing list