[CalendarServer-changes] [12835] CalendarServer/branches/users/sagen/move2who

source_changes at macosforge.org source_changes at macosforge.org
Thu Mar 6 17:22:21 PST 2014


Revision: 12835
          http://trac.calendarserver.org//changeset/12835
Author:   sagen at apple.com
Date:     2014-03-06 17:22:21 -0800 (Thu, 06 Mar 2014)
Log Message:
-----------
Halfway through rewrite of calendarserver_manage_principals.  Added recordsMatchingTokens()

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/move2who/calendarserver/tap/caldav.py
    CalendarServer/branches/users/sagen/move2who/calendarserver/tools/principals.py
    CalendarServer/branches/users/sagen/move2who/calendarserver/tools/util.py
    CalendarServer/branches/users/sagen/move2who/txdav/dps/client.py
    CalendarServer/branches/users/sagen/move2who/txdav/dps/commands.py
    CalendarServer/branches/users/sagen/move2who/txdav/dps/server.py
    CalendarServer/branches/users/sagen/move2who/txdav/dps/test/test_client.py

Modified: CalendarServer/branches/users/sagen/move2who/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who/calendarserver/tap/caldav.py	2014-03-06 23:06:36 UTC (rev 12834)
+++ CalendarServer/branches/users/sagen/move2who/calendarserver/tap/caldav.py	2014-03-07 01:22:21 UTC (rev 12835)
@@ -549,8 +549,8 @@
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
         if (
-           config.DirectoryProxy.Enabled and
-           config.DirectoryProxy.SocketPath != ""
+            config.DirectoryProxy.Enabled and
+            config.DirectoryProxy.SocketPath != ""
         ):
             log.info("Adding directory proxy service")
 

Modified: CalendarServer/branches/users/sagen/move2who/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who/calendarserver/tools/principals.py	2014-03-06 23:06:36 UTC (rev 12834)
+++ CalendarServer/branches/users/sagen/move2who/calendarserver/tools/principals.py	2014-03-07 01:22:21 UTC (rev 12835)
@@ -26,17 +26,16 @@
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from txdav.xml import element as davxml
+from txdav.who.delegates import addDelegate, removeDelegate
 
-from txdav.xml.base import decodeXMLName, encodeXMLName
 
 from twistedcaldav.config import config
-from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryError
+from twistedcaldav.directory.directory import UnknownRecordTypeError
 from txdav.who.groups import schedulePolledGroupCachingUpdate
 
 from calendarserver.tools.util import (
-    booleanArgument, proxySubprincipal, action_addProxyPrincipal,
-    principalForPrincipalID, prettyPrincipal, ProxyError,
-    action_removeProxyPrincipal
+    booleanArgument, proxySubprincipal,
+    recordForPrincipalID, prettyPrincipal, prettyRecord, ProxyError
 )
 from twistedcaldav.directory.augment import allowedAutoScheduleModes
 
@@ -74,10 +73,10 @@
     print("  --search <search-string>: search for matching principals")
     print("  --list-principal-types: list all of the known principal types")
     print("  --list-principals type: list all principals of the given type")
-    print("  --read-property=property: read DAV property (eg.: {DAV:}group-member-set)")
     print("  --list-read-proxies: list proxies with read-only access")
     print("  --list-write-proxies: list proxies with read-write access")
     print("  --list-proxies: list all proxies")
+    print("  --list-proxy-for: principals this principal is a proxy for")
     print("  --add-read-proxy=principal: add a read-only proxy")
     print("  --add-write-proxy=principal: add a read-write proxy")
     print("  --remove-proxy=principal: remove a proxy")
@@ -118,33 +117,32 @@
         resource, directory, store, and whatever has been assigned to "params".
         """
         if self.function is not None:
-            rootResource = self.rootResource()
-            directory = rootResource.getDirectory()
-            yield self.function(rootResource, directory, self.store, *self.params)
+            yield self.function(self.store, *self.params)
 
 attrMap = {
-    'GeneratedUID' : { 'attr' : 'guid', },
-    'RealName' : { 'attr' : 'fullName', },
-    'RecordName' : { 'attr' : 'shortNames', },
-    'AutoSchedule' : { 'attr' : 'autoSchedule', },
-    'AutoAcceptGroup' : { 'attr' : 'autoAcceptGroup', },
+    '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', },
+    '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', },
+    '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', },
+    'AbbreviatedName': {'extras': True, 'attr': 'abbreviatedName', },
+    'StreetAddress': {'extras': True, 'attr': 'streetAddress', },
+    'Geo': {'extras': True, 'attr': 'geo', },
 }
 
 
+ at inlineCallbacks
 def main():
     try:
         (optargs, args) = getopt(
@@ -156,10 +154,10 @@
                 "search=",
                 "list-principal-types",
                 "list-principals=",
-                "read-property=",
                 "list-read-proxies",
                 "list-write-proxies",
                 "list-proxies",
+                "list-proxy-for",
                 "add-read-proxy=",
                 "add-write-proxy=",
                 "remove-proxy=",
@@ -185,7 +183,7 @@
     # Get configuration
     #
     configFileName = None
-    addType = None
+    # addType = None
     listPrincipalTypes = False
     listPrincipals = None
     searchPrincipals = None
@@ -202,11 +200,11 @@
         elif opt in ("-f", "--config"):
             configFileName = arg
 
-        elif opt in ("-a", "--add"):
-            addType = arg
+        # elif opt in ("-a", "--add"):
+        #     addType = arg
 
-        elif opt in ("-r", "--remove"):
-            principalActions.append((action_removePrincipal,))
+        # elif opt in ("-r", "--remove"):
+        #     principalActions.append((action_removePrincipal,))
 
         elif opt in ("", "--list-principal-types"):
             listPrincipalTypes = True
@@ -217,13 +215,6 @@
         elif opt in ("", "--search"):
             searchPrincipals = arg
 
-        elif opt in ("", "--read-property"):
-            try:
-                qname = decodeXMLName(arg)
-            except ValueError, e:
-                abort(e)
-            principalActions.append((action_readProperty, qname))
-
         elif opt in ("", "--list-read-proxies"):
             principalActions.append((action_listProxies, "read"))
 
@@ -233,6 +224,9 @@
         elif opt in ("-L", "--list-proxies"):
             principalActions.append((action_listProxies, "read", "write"))
 
+        elif opt in ("--list-proxy-for"):
+            principalActions.append((action_listProxyFor, "read", "write"))
+
         elif opt in ("--add-read-proxy", "--add-write-proxy"):
             if "read" in opt:
                 proxyType = "read"
@@ -242,7 +236,7 @@
                 raise AssertionError("Unknown proxy type")
 
             try:
-                principalForPrincipalID(arg, checkOnly=True)
+                yield recordForPrincipalID(arg, checkOnly=True)
             except ValueError, e:
                 abort(e)
 
@@ -250,64 +244,64 @@
 
         elif opt in ("", "--remove-proxy"):
             try:
-                principalForPrincipalID(arg, checkOnly=True)
+                yield recordForPrincipalID(arg, checkOnly=True)
             except ValueError, e:
                 abort(e)
 
             principalActions.append((action_removeProxy, arg))
 
-        elif opt in ("", "--set-auto-schedule"):
-            try:
-                autoSchedule = booleanArgument(arg)
-            except ValueError, e:
-                abort(e)
+        # elif opt in ("", "--set-auto-schedule"):
+        #     try:
+        #         autoSchedule = booleanArgument(arg)
+        #     except ValueError, e:
+        #         abort(e)
 
-            principalActions.append((action_setAutoSchedule, autoSchedule))
+        #     principalActions.append((action_setAutoSchedule, autoSchedule))
 
-        elif opt in ("", "--get-auto-schedule"):
-            principalActions.append((action_getAutoSchedule,))
+        # elif opt in ("", "--get-auto-schedule"):
+        #     principalActions.append((action_getAutoSchedule,))
 
-        elif opt in ("", "--set-auto-schedule-mode"):
-            try:
-                if arg not in allowedAutoScheduleModes:
-                    raise ValueError("Unknown auto-schedule mode: %s" % (arg,))
-                autoScheduleMode = arg
-            except ValueError, e:
-                abort(e)
+        # elif opt in ("", "--set-auto-schedule-mode"):
+        #     try:
+        #         if arg not in allowedAutoScheduleModes:
+        #             raise ValueError("Unknown auto-schedule mode: %s" % (arg,))
+        #         autoScheduleMode = arg
+        #     except ValueError, e:
+        #         abort(e)
 
-            principalActions.append((action_setAutoScheduleMode, autoScheduleMode))
+        #     principalActions.append((action_setAutoScheduleMode, autoScheduleMode))
 
-        elif opt in ("", "--get-auto-schedule-mode"):
-            principalActions.append((action_getAutoScheduleMode,))
+        # elif opt in ("", "--get-auto-schedule-mode"):
+        #     principalActions.append((action_getAutoScheduleMode,))
 
-        elif opt in ("", "--set-auto-accept-group"):
-            try:
-                principalForPrincipalID(arg, checkOnly=True)
-            except ValueError, e:
-                abort(e)
+        # elif opt in ("", "--set-auto-accept-group"):
+        #     try:
+        #         yield recordForPrincipalID(arg, checkOnly=True)
+        #     except ValueError, e:
+        #         abort(e)
 
-            principalActions.append((action_setAutoAcceptGroup, arg))
+        #     principalActions.append((action_setAutoAcceptGroup, arg))
 
-        elif opt in ("", "--get-auto-accept-group"):
-            principalActions.append((action_getAutoAcceptGroup,))
+        # 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 ("", "--set-geo"):
+        #     principalActions.append((action_setValue, "Geo", arg))
 
-        elif opt in ("", "--get-geo"):
-            principalActions.append((action_getValue, "Geo"))
+        # 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 ("", "--set-street-address"):
+        #     principalActions.append((action_setValue, "StreetAddress", arg))
 
-        elif opt in ("", "--get-street-address"):
-            principalActions.append((action_getValue, "StreetAddress"))
+        # 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 ("", "--set-address"):
+        #     principalActions.append((action_setValue, "AssociatedAddress", arg))
 
-        elif opt in ("", "--get-address"):
-            principalActions.append((action_getValue, "AssociatedAddress"))
+        # elif opt in ("", "--get-address"):
+        #     principalActions.append((action_getValue, "AssociatedAddress"))
 
         else:
             raise NotImplementedError(opt)
@@ -322,32 +316,34 @@
         function = runListPrincipalTypes
         params = ()
 
-    elif addType:
+    # elif addType:
 
-        try:
-            addType = matchStrings(addType, ["locations", "resources", "addresses"])
-        except ValueError, e:
-            print(e)
-            return
+    #     try:
+    #         addType = matchStrings(addType, ["locations", "resources", "addresses"])
+    #     except ValueError, e:
+    #         print(e)
+    #         return
 
-        try:
-            fullName, shortName, guid = parseCreationArgs(args)
-        except ValueError, e:
-            print(e)
-            return
+    #     try:
+    #         fullName, shortName, guid = parseCreationArgs(args)
+    #     except ValueError, e:
+    #         print(e)
+    #         return
 
-        if shortName is not None:
-            shortNames = [shortName]
-        else:
-            shortNames = ()
+    #     if shortName is not None:
+    #         shortNames = [shortName]
+    #     else:
+    #         shortNames = ()
 
-        function = runAddPrincipal
-        params = (addType, guid, shortNames, fullName)
+    #     function = runAddPrincipal
+    #     params = (addType, guid, shortNames, fullName)
 
     elif listPrincipals:
         try:
-            listPrincipals = matchStrings(listPrincipals, ["users", "groups",
-                "locations", "resources", "addresses"])
+            listPrincipals = matchStrings(
+                listPrincipals,
+                ["users", "groups", "locations", "resources", "addresses"]
+            )
         except ValueError, e:
             print(e)
             return
@@ -372,7 +368,7 @@
 
         for arg in args:
             try:
-                principalForPrincipalID(arg, checkOnly=True)
+                yield recordForPrincipalID(arg, checkOnly=True)
             except ValueError, e:
                 abort(e)
 
@@ -385,17 +381,20 @@
 
 
 
-def runListPrincipalTypes(service, rootResource, directory, store):
+def runListPrincipalTypes(service, store):
+    directory = store.directoryService()
     for recordType in directory.recordTypes():
-        print(recordType)
+        print(directory.recordTypeToOldString(recordType))
     return succeed(None)
 
 
 
 @inlineCallbacks
-def runListPrincipals(service, rootResource, directory, store, listPrincipals):
+def runListPrincipals(service, store, listPrincipals):
+    directory = store.directoryService()
+    recordType = directory.oldNameToRecordType(listPrincipals)
     try:
-        records = list((yield directory.listRecords(listPrincipals)))
+        records = list((yield directory.recordsWithRecordType(recordType)))
         if records:
             printRecordList(records)
         else:
@@ -407,51 +406,48 @@
 
 
 @inlineCallbacks
-def runPrincipalActions(service, rootResource, directory, store, principalIDs,
-    actions):
+def runPrincipalActions(service, store, principalIDs, actions):
+    directory = store.directoryService()
     for principalID in principalIDs:
-        # Resolve the given principal IDs to principals
+        # Resolve the given principal IDs to records
         try:
-            principal = yield principalForPrincipalID(principalID, directory=directory)
+            record = yield recordForPrincipalID(
+                principalID, directory=directory
+            )
         except ValueError:
-            principal = None
+            record = None
 
-        if principal is None:
+        if record is None:
             sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
             continue
 
         # Performs requested actions
         for action in actions:
-            (yield action[0](rootResource, directory, store, principal,
-                *action[1:]))
+            (yield action[0](store, record, *action[1:]))
             print("")
 
 
 
 @inlineCallbacks
-def runSearch(service, rootResource, directory, store, searchTerm):
-
+def runSearch(service, store, searchTerm):
+    directory = store.directoryService()
     fields = []
-    for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
+    for fieldName in ("fullNames", "emailAddresses"):
         fields.append((fieldName, searchTerm, True, "contains"))
 
     records = list((yield directory.recordsMatchingTokens(searchTerm.strip().split())))
     if records:
-        records.sort(key=operator.attrgetter('fullName'))
+        records.sort(key=operator.attrgetter('fullNames'))
         print("%d matches found:" % (len(records),))
         for record in records:
-            print("\n%s (%s)" % (record.fullName,
-                {"users" : "User",
-                 "groups" : "Group",
-                 "locations" : "Place",
-                 "resources" : "Resource",
-                 "addresses" : "Address",
-                }.get(record.recordType),
-            ))
-            print("   GUID: %s" % (record.guid,))
+            print(
+                "\n{d} {rt}".format(
+                    d=record.displayName,
+                    rt=record.recordType.name
+                )
+            )
+            print("   UID: %s" % (record.uid,))
             print("   Record name(s): %s" % (", ".join(record.shortNames),))
-            if record.authIDs:
-                print("   Auth ID(s): %s" % (", ".join(record.authIDs),))
             if record.emailAddresses:
                 print("   Email(s): %s" % (", ".join(record.emailAddresses),))
     else:
@@ -461,292 +457,318 @@
 
 
 
- at inlineCallbacks
-def runAddPrincipal(service, rootResource, directory, store, addType, guid,
-    shortNames, fullName):
-    try:
-        yield updateRecord(True, directory, addType, guid=guid,
-            shortNames=shortNames, fullName=fullName)
-        print("Added '%s'" % (fullName,))
-    except DirectoryError, e:
-        print(e)
+# @inlineCallbacks
+# def runAddPrincipal(service, store, addType, guid, shortNames, fullName):
+#     directory = store.directoryService()
+#     try:
+#         # FIXME STOP USING GUID
+#         yield updateRecord(
+#             True, directory, addType, guid=guid,
+#             shortNames=shortNames, fullName=fullName
+#         )
+#         print("Added '%s'" % (fullName,))
+#     except DirectoryError, e:
+#         print(e)
 
 
 
-def action_removePrincipal(rootResource, directory, store, principal):
-    record = principal.record
-    fullName = record.fullName
-    shortName = record.shortNames[0]
-    guid = record.guid
+# def action_removePrincipal(store, record):
+#     directory = store.directoryService()
+#     fullName = record.displayName
+#     shortName = record.shortNames[0]
 
-    directory.destroyRecord(record.recordType, guid=guid)
-    print("Removed '%s' %s %s" % (fullName, shortName, guid))
+#     yield directory.destroyRecord(record.recordType, uid=record.uid)
+#     print("Removed '%s' %s %s" % (fullName, shortName, record.uid))
 
 
 
+
 @inlineCallbacks
-def action_readProperty(rootResource, directory, store, resource, qname):
-    property = (yield resource.readProperty(qname, None))
-    print("%r on %s:" % (encodeXMLName(*qname), resource))
-    print("")
-    print(property.toxml())
+def action_listProxies(store, record, *proxyTypes):
+    directory = store.directoryService()
+    for proxyType in proxyTypes:
 
+        groupRecordType = {
+            "read": directory.recordType.readDelegateGroup,
+            "write": directory.recordType.writeDelegateGroup,
+        }.get(proxyType)
 
+        pseudoGroup = yield directory.recordWithShortName(
+            groupRecordType,
+            record.uid
+        )
+        proxies = yield pseudoGroup.members()
+        if proxies:
+            print("%s proxies for %s:" % (
+                {"read": "Read-only", "write": "Read/write"}[proxyType],
+                prettyRecord(record)
+            ))
+            printRecordList(proxies)
+            print("")
+        else:
+            print("No %s proxies for %s" % (proxyType, prettyRecord(record)))
 
+
 @inlineCallbacks
-def action_listProxies(rootResource, directory, store, principal, *proxyTypes):
+def action_listProxyFor(store, record, *proxyTypes):
+    directory = store.directoryService()
     for proxyType in proxyTypes:
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            print("No %s proxies for %s" % (proxyType,
-                prettyPrincipal(principal)))
-            continue
 
-        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+        groupRecordType = {
+            "read": directory.recordType.readDelegatorGroup,
+            "write": directory.recordType.writeDelegatorGroup,
+        }.get(proxyType)
 
-        if membersProperty.children:
-            print("%s proxies for %s:" % (
-                {"read": "Read-only", "write": "Read/write"}[proxyType],
-                prettyPrincipal(principal)
+        pseudoGroup = yield directory.recordWithShortName(
+            groupRecordType,
+            record.uid
+        )
+        proxies = yield pseudoGroup.members()
+        if proxies:
+            print("%s is a %s proxy for:" % (
+                prettyRecord(record),
+                {"read": "Read-only", "write": "Read/write"}[proxyType]
             ))
-            records = []
-            for member in membersProperty.children:
-                proxyPrincipal = principalForPrincipalID(str(member),
-                    directory=directory)
-                records.append(proxyPrincipal.record)
-
-            printRecordList(records)
-            print
+            printRecordList(proxies)
+            print("")
         else:
-            print("No %s proxies for %s" % (proxyType,
-                prettyPrincipal(principal)))
+            print(
+                "{r} is not a {t} proxy for anyone".format(
+                    r=prettyRecord(record),
+                    t={"read": "Read-only", "write": "Read/write"}[proxyType]
+                )
+            )
 
 
-
 @inlineCallbacks
-def action_addProxy(rootResource, directory, store, principal, proxyType, *proxyIDs):
+def _addRemoveProxy(fn, store, record, proxyType, *proxyIDs):
+    directory = store.directoryService()
+    readWrite = (proxyType == "write")
     for proxyID in proxyIDs:
-        proxyPrincipal = yield principalForPrincipalID(proxyID, directory=directory)
-        if proxyPrincipal is None:
+        proxyRecord = yield recordForPrincipalID(proxyID, directory=directory)
+        if proxyRecord is None:
             print("Invalid principal ID: %s" % (proxyID,))
         else:
-            (yield action_addProxyPrincipal(rootResource, directory, store,
-                principal, proxyType, proxyPrincipal))
+            txn = store.newTransaction()
+            yield fn(txn, record, proxyRecord, readWrite)
+            yield txn.commit()
 
 
+def action_addProxy(store, record, proxyType, *proxyIDs):
+    return _addRemoveProxy(addDelegate, store, record, proxyType, *proxyIDs)
 
+
 @inlineCallbacks
-def setProxies(store, principal, readProxyPrincipals, writeProxyPrincipals, directory=None):
-    """
-    Set read/write proxies en masse for a principal
-    @param principal: DirectoryPrincipalResource
-    @param readProxyPrincipals: a list of principal IDs (see principalForPrincipalID)
-    @param writeProxyPrincipals: a list of principal IDs (see principalForPrincipalID)
-    """
+def action_removeProxy(store, record, *proxyIDs):
+    # Write
+    yield _addRemoveProxy(removeDelegate, store, record, "write", *proxyIDs)
+    # Read
+    yield _addRemoveProxy(removeDelegate, store, record, "read", *proxyIDs)
 
-    proxyTypes = [
-        ("read", readProxyPrincipals),
-        ("write", writeProxyPrincipals),
-    ]
-    for proxyType, proxyIDs in proxyTypes:
-        if proxyIDs is None:
-            continue
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
-                prettyPrincipal(principal)))
-        memberURLs = []
-        for proxyID in proxyIDs:
-            proxyPrincipal = yield principalForPrincipalID(proxyID, directory=directory)
-            proxyURL = proxyPrincipal.url()
-            memberURLs.append(davxml.HRef(proxyURL))
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        yield subPrincipal.writeProperty(membersProperty, None)
-        if store is not None:
-            # Schedule work the PeerConnectionPool will pick up as overdue
-            yield schedulePolledGroupCachingUpdate(store)
 
 
+# @inlineCallbacks
+# def setProxies(store, principal, readProxyPrincipals, writeProxyPrincipals, directory=None):
+#     """
+#     Set read/write proxies en masse for a principal
+#     @param principal: DirectoryPrincipalResource
+#     @param readProxyPrincipals: a list of principal IDs (see principalForPrincipalID)
+#     @param writeProxyPrincipals: a list of principal IDs (see principalForPrincipalID)
+#     """
 
- at inlineCallbacks
-def getProxies(principal, directory=None):
-    """
-    Returns a tuple containing the GUIDs for read proxies and write proxies
-    of the given principal
-    """
+#     proxyTypes = [
+#         ("read", readProxyPrincipals),
+#         ("write", writeProxyPrincipals),
+#     ]
+#     for proxyType, proxyIDs in proxyTypes:
+#         if proxyIDs is None:
+#             continue
+#         subPrincipal = proxySubprincipal(principal, proxyType)
+#         if subPrincipal is None:
+#             raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
+#                 prettyPrincipal(principal)))
+#         memberURLs = []
+#         for proxyID in proxyIDs:
+#             proxyPrincipal = yield principalForPrincipalID(proxyID, directory=directory)
+#             proxyURL = proxyPrincipal.url()
+#             memberURLs.append(davxml.HRef(proxyURL))
+#         membersProperty = davxml.GroupMemberSet(*memberURLs)
+#         yield subPrincipal.writeProperty(membersProperty, None)
+#         if store is not None:
+#             # Schedule work the PeerConnectionPool will pick up as overdue
+#             yield schedulePolledGroupCachingUpdate(store)
 
-    proxies = {
-        "read" : [],
-        "write" : [],
-    }
-    for proxyType in proxies.iterkeys():
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is not None:
-            membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-            if membersProperty.children:
-                for member in membersProperty.children:
-                    proxyPrincipal = yield principalForPrincipalID(str(member), directory=directory)
-                    proxies[proxyType].append(proxyPrincipal.record.guid)
 
-    returnValue((proxies['read'], proxies['write']))
 
+# @inlineCallbacks
+# def getProxies(principal, directory=None):
+#     """
+#     Returns a tuple containing the GUIDs for read proxies and write proxies
+#     of the given principal
+#     """
 
+#     proxies = {
+#         "read": [],
+#         "write": [],
+#     }
+#     for proxyType in proxies.iterkeys():
+#         subPrincipal = proxySubprincipal(principal, proxyType)
+#         if subPrincipal is not None:
+#             membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+#             if membersProperty.children:
+#                 for member in membersProperty.children:
+#                     proxyPrincipal = yield principalForPrincipalID(str(member), directory=directory)
+#                     proxies[proxyType].append(proxyPrincipal.record.guid)
 
- at inlineCallbacks
-def action_removeProxy(rootResource, directory, store, principal, *proxyIDs, **kwargs):
-    for proxyID in proxyIDs:
-        proxyPrincipal = yield principalForPrincipalID(proxyID, directory=directory)
-        if proxyPrincipal is None:
-            print("Invalid principal ID: %s" % (proxyID,))
-        else:
-            (yield action_removeProxyPrincipal(rootResource, directory, store,
-                principal, proxyPrincipal, **kwargs))
+#     returnValue((proxies['read'], proxies['write']))
 
 
 
- at inlineCallbacks
-def action_setAutoSchedule(rootResource, directory, store, principal, autoSchedule):
-    if principal.record.recordType == "groups":
-        print("Enabling auto-schedule for %s is not allowed." % (principal,))
 
-    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
-        print("Enabling auto-schedule for %s is not allowed." % (principal,))
 
-    else:
-        print("Setting auto-schedule to %s for %s" % (
-            {True: "true", False: "false"}[autoSchedule],
-            prettyPrincipal(principal),
-        ))
+# @inlineCallbacks
+# def action_setAutoSchedule(rootResource, directory, store, principal, autoSchedule):
+#     if principal.record.recordType == "groups":
+#         print("Enabling auto-schedule for %s is not allowed." % (principal,))
 
-        (yield updateRecord(False, directory,
-            principal.record.recordType,
-            guid=principal.record.guid,
-            shortNames=principal.record.shortNames,
-            fullName=principal.record.fullName,
-            autoSchedule=autoSchedule,
-            **principal.record.extras
-        ))
+#     elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
+#         print("Enabling auto-schedule for %s is not allowed." % (principal,))
 
+#     else:
+#         print("Setting auto-schedule to %s for %s" % (
+#             {True: "true", False: "false"}[autoSchedule],
+#             prettyPrincipal(principal),
+#         ))
 
+#         (yield updateRecord(False, directory,
+#             principal.record.recordType,
+#             guid=principal.record.guid,
+#             shortNames=principal.record.shortNames,
+#             fullName=principal.record.fullName,
+#             autoSchedule=autoSchedule,
+#             **principal.record.extras
+#         ))
 
-def action_getAutoSchedule(rootResource, directory, store, principal):
-    autoSchedule = principal.getAutoSchedule()
-    print("Auto-schedule for %s is %s" % (
-        prettyPrincipal(principal),
-        {True: "true", False: "false"}[autoSchedule],
-    ))
 
 
+# def action_getAutoSchedule(rootResource, directory, store, principal):
+#     autoSchedule = principal.getAutoSchedule()
+#     print("Auto-schedule for %s is %s" % (
+#         prettyPrincipal(principal),
+#         {True: "true", False: "false"}[autoSchedule],
+#     ))
 
- at inlineCallbacks
-def action_setAutoScheduleMode(rootResource, directory, store, principal, autoScheduleMode):
-    if principal.record.recordType == "groups":
-        print("Setting auto-schedule mode for %s is not allowed." % (principal,))
 
-    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
-        print("Setting auto-schedule mode for %s is not allowed." % (principal,))
 
-    else:
-        print("Setting auto-schedule mode to %s for %s" % (
-            autoScheduleMode,
-            prettyPrincipal(principal),
-        ))
+# @inlineCallbacks
+# def action_setAutoScheduleMode(rootResource, directory, store, principal, autoScheduleMode):
+#     if principal.record.recordType == "groups":
+#         print("Setting auto-schedule mode for %s is not allowed." % (principal,))
 
-        (yield updateRecord(False, directory,
-            principal.record.recordType,
-            guid=principal.record.guid,
-            shortNames=principal.record.shortNames,
-            fullName=principal.record.fullName,
-            autoScheduleMode=autoScheduleMode,
-            **principal.record.extras
-        ))
+#     elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
+#         print("Setting auto-schedule mode for %s is not allowed." % (principal,))
 
+#     else:
+#         print("Setting auto-schedule mode to %s for %s" % (
+#             autoScheduleMode,
+#             prettyPrincipal(principal),
+#         ))
 
+#         (yield updateRecord(False, directory,
+#             principal.record.recordType,
+#             guid=principal.record.guid,
+#             shortNames=principal.record.shortNames,
+#             fullName=principal.record.fullName,
+#             autoScheduleMode=autoScheduleMode,
+#             **principal.record.extras
+#         ))
 
-def action_getAutoScheduleMode(rootResource, directory, store, principal):
-    autoScheduleMode = principal.getAutoScheduleMode()
-    if not autoScheduleMode:
-        autoScheduleMode = "automatic"
-    print("Auto-schedule mode for %s is %s" % (
-        prettyPrincipal(principal),
-        autoScheduleMode,
-    ))
 
 
+# def action_getAutoScheduleMode(rootResource, directory, store, principal):
+#     autoScheduleMode = principal.getAutoScheduleMode()
+#     if not autoScheduleMode:
+#         autoScheduleMode = "automatic"
+#     print("Auto-schedule mode for %s is %s" % (
+#         prettyPrincipal(principal),
+#         autoScheduleMode,
+#     ))
 
- at inlineCallbacks
-def action_setAutoAcceptGroup(rootResource, directory, store, principal, autoAcceptGroup):
-    if principal.record.recordType == "groups":
-        print("Setting auto-accept-group for %s is not allowed." % (principal,))
 
-    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
-        print("Setting auto-accept-group for %s is not allowed." % (principal,))
 
-    else:
-        groupPrincipal = yield principalForPrincipalID(autoAcceptGroup, directory=directory)
-        if groupPrincipal is None or groupPrincipal.record.recordType != "groups":
-            print("Invalid principal ID: %s" % (autoAcceptGroup,))
-        else:
-            print("Setting auto-accept-group to %s for %s" % (
-                prettyPrincipal(groupPrincipal),
-                prettyPrincipal(principal),
-            ))
+# @inlineCallbacks
+# def action_setAutoAcceptGroup(rootResource, directory, store, principal, autoAcceptGroup):
+#     if principal.record.recordType == "groups":
+#         print("Setting auto-accept-group for %s is not allowed." % (principal,))
 
-            (yield updateRecord(False, directory,
-                principal.record.recordType,
-                guid=principal.record.guid,
-                shortNames=principal.record.shortNames,
-                fullName=principal.record.fullName,
-                autoAcceptGroup=groupPrincipal.record.guid,
-                **principal.record.extras
-            ))
+#     elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
+#         print("Setting auto-accept-group for %s is not allowed." % (principal,))
 
+#     else:
+#         groupPrincipal = yield principalForPrincipalID(autoAcceptGroup, directory=directory)
+#         if groupPrincipal is None or groupPrincipal.record.recordType != "groups":
+#             print("Invalid principal ID: %s" % (autoAcceptGroup,))
+#         else:
+#             print("Setting auto-accept-group to %s for %s" % (
+#                 prettyPrincipal(groupPrincipal),
+#                 prettyPrincipal(principal),
+#             ))
 
+#             (yield updateRecord(False, directory,
+#                 principal.record.recordType,
+#                 guid=principal.record.guid,
+#                 shortNames=principal.record.shortNames,
+#                 fullName=principal.record.fullName,
+#                 autoAcceptGroup=groupPrincipal.record.guid,
+#                 **principal.record.extras
+#             ))
 
-def action_getAutoAcceptGroup(rootResource, directory, store, principal):
-    autoAcceptGroup = principal.getAutoAcceptGroup()
-    if autoAcceptGroup:
-        record = yield directory.recordWithGUID(autoAcceptGroup)
-        if record is not None:
-            groupPrincipal = yield directory.principalCollection.principalForUID(record.uid)
-            if groupPrincipal is not None:
-                print("Auto-accept-group for %s is %s" % (
-                    prettyPrincipal(principal),
-                    prettyPrincipal(groupPrincipal),
-                ))
-                return
-        print("Invalid auto-accept-group assigned: %s" % (autoAcceptGroup,))
-    else:
-        print("No auto-accept-group assigned to %s" % (prettyPrincipal(principal),))
 
 
+# def action_getAutoAcceptGroup(rootResource, directory, store, principal):
+#     autoAcceptGroup = principal.getAutoAcceptGroup()
+#     if autoAcceptGroup:
+#         record = yield directory.recordWithGUID(autoAcceptGroup)
+#         if record is not None:
+#             groupPrincipal = yield directory.principalCollection.principalForUID(record.uid)
+#             if groupPrincipal is not None:
+#                 print("Auto-accept-group for %s is %s" % (
+#                     prettyPrincipal(principal),
+#                     prettyPrincipal(groupPrincipal),
+#                 ))
+#                 return
+#         print("Invalid auto-accept-group assigned: %s" % (autoAcceptGroup,))
+#     else:
+#         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
-    ))
 
+# @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 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:
@@ -804,14 +826,16 @@
 
 
 def printRecordList(records):
-    results = [(record.fullName, record.shortNames[0], record.guid)
-        for record in records]
+    results = [
+        (record.displayName, record.recordType.name, record.uid, record.shortNames)
+        for record in records
+    ]
     results.sort()
-    format = "%-22s %-17s %s"
-    print(format % ("Full name", "Record name", "UUID"))
-    print(format % ("---------", "-----------", "----"))
-    for fullName, shortName, guid in results:
-        print(format % (fullName, shortName, guid))
+    format = "%-22s %-10s %-20s %s"
+    print(format % ("Full name", "Type", "UID", "Short names"))
+    print(format % ("---------", "----", "---", "-----------"))
+    for fullName, recordType, uid, shortNames in results:
+        print(format % (fullName, recordType, uid, u", ".join(shortNames)))
 
 
 

Modified: CalendarServer/branches/users/sagen/move2who/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who/calendarserver/tools/util.py	2014-03-06 23:06:36 UTC (rev 12834)
+++ CalendarServer/branches/users/sagen/move2who/calendarserver/tools/util.py	2014-03-07 01:22:21 UTC (rev 12835)
@@ -334,6 +334,7 @@
 
 
 
+ at inlineCallbacks
 def principalForPrincipalID(principalID, checkOnly=False, directory=None):
 
     # Allow a directory parameter to be passed in, but default to config.directory
@@ -351,16 +352,16 @@
             raise ValueError("Can't resolve all paths yet")
 
         if checkOnly:
-            return None
+            returnValue(None)
 
-        return directory.principalCollection.principalForUID(uid)
+        returnValue((yield directory.principalCollection.principalForUID(uid)))
 
     if principalID.startswith("("):
         try:
             i = principalID.index(")")
 
             if checkOnly:
-                return None
+                returnValue(None)
 
             recordType = principalID[1:i]
             shortName = principalID[i + 1:]
@@ -368,34 +369,93 @@
             if not recordType or not shortName or "(" in recordType:
                 raise ValueError()
 
-            return directory.principalCollection.principalForShortName(recordType, shortName)
+            returnValue((yield directory.principalCollection.principalForShortName(recordType, shortName)))
 
         except ValueError:
             pass
 
     if ":" in principalID:
         if checkOnly:
-            return None
+            returnValue(None)
 
         recordType, shortName = principalID.split(":", 1)
 
-        return directory.principalCollection.principalForShortName(recordType, shortName)
+        returnValue((yield directory.principalCollection.principalForShortName(recordType, shortName)))
 
     try:
         UUID(principalID)
 
         if checkOnly:
-            return None
+            returnValue(None)
 
-        x = directory.principalCollection.principalForUID(principalID)
-        return x
+        returnValue((yield directory.principalCollection.principalForUID(principalID)))
     except ValueError:
         pass
 
     raise ValueError("Invalid principal identifier: %s" % (principalID,))
 
 
+ at inlineCallbacks
+def recordForPrincipalID(principalID, checkOnly=False, directory=None):
 
+    # Allow a directory parameter to be passed in, but default to config.directory
+    # But config.directory isn't set right away, so only use it when we're doing more
+    # than checking.
+    if not checkOnly and not directory:
+        directory = config.directory
+
+    if principalID.startswith("/"):
+        segments = principalID.strip("/").split("/")
+        if (len(segments) == 3 and
+            segments[0] == "principals" and segments[1] == "__uids__"):
+            uid = segments[2]
+        else:
+            raise ValueError("Can't resolve all paths yet")
+
+        if checkOnly:
+            returnValue(None)
+
+        returnValue((yield directory.recordWithUID(uid)))
+
+    if principalID.startswith("("):
+        try:
+            i = principalID.index(")")
+
+            if checkOnly:
+                returnValue(None)
+
+            recordType = directory.oldNameToRecordType(principalID[1:i])
+            shortName = principalID[i + 1:]
+
+            if not recordType or not shortName or "(" in recordType:
+                raise ValueError()
+
+            returnValue((yield directory.recordWithShortName(recordType, shortName)))
+
+        except ValueError:
+            pass
+
+    if ":" in principalID:
+        if checkOnly:
+            returnValue(None)
+
+        recordType, shortName = principalID.split(":", 1)
+        recordType = directory.oldNameToRecordType(recordType)
+
+        returnValue((yield directory.recordWithShortName(recordType, shortName)))
+
+    try:
+        if checkOnly:
+            returnValue(None)
+
+        returnValue((yield directory.recordWithUID(principalID)))
+    except ValueError:
+        pass
+
+    raise ValueError("Invalid principal identifier: %s" % (principalID,))
+
+
+
 def proxySubprincipal(principal, proxyType):
     return principal.getChild("calendar-proxy-" + proxyType)
 
@@ -501,12 +561,19 @@
 
 
 def prettyPrincipal(principal):
-    record = principal.record
-    return "\"%s\" (%s:%s)" % (record.fullName, record.recordType,
-        record.shortNames[0])
+    prettyRecord(principal.record)
 
 
+def prettyRecord(record):
+    return "\"{d}\" {uid} ({rt}) {sn}".format(
+        d=record.displayName,
+        rt=record.recordType.name,
+        uid=record.uid,
+        sn=(", ".join(record.shortNames))
+    )
 
+
+
 class ProxyError(Exception):
     """
     Raised when proxy assignments cannot be performed

Modified: CalendarServer/branches/users/sagen/move2who/txdav/dps/client.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who/txdav/dps/client.py	2014-03-06 23:06:36 UTC (rev 12834)
+++ CalendarServer/branches/users/sagen/move2who/txdav/dps/client.py	2014-03-07 01:22:21 UTC (rev 12835)
@@ -29,12 +29,12 @@
 from twisted.internet.protocol import ClientCreator
 from twisted.protocols import amp
 from twisted.python.constants import Names, NamedConstant
-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
 from txdav.caldav.icalendardirectoryservice import ICalendarStoreDirectoryRecord
 from txdav.common.idirectoryservice import IStoreDirectoryService
 from txdav.dps.commands import (
     RecordWithShortNameCommand, RecordWithUIDCommand, RecordWithGUIDCommand,
     RecordsWithRecordTypeCommand, RecordsWithEmailAddressCommand,
+    RecordsMatchingTokensCommand,
     MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand
 )
@@ -239,13 +239,15 @@
         )
 
 
-    def listRecords(self, recordType):
-        # MOVE2WHO
-        return []
+    # def listRecords(self, recordType):
+    #     # MOVE2WHO
+    #     return []
 
 
     @inlineCallbacks
     def recordWithCalendarUserAddress(self, address):
+        # FIXME: Circular
+        from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
         address = normalizeCUAddr(address)
         record = None
         if address.startswith("urn:uuid:"):
@@ -270,18 +272,27 @@
         returnValue(record if record and record.hasCalendars else None)
 
 
-    @inlineCallbacks
     def recordsMatchingTokens(self, tokens, context=None, limitResults=50,
                               timeoutSeconds=10):
-        rec = yield self.recordWithShortName(
-            twext.who.idirectory.RecordType.user,
-            u"wsanchez"
+        return self._call(
+            RecordsMatchingTokensCommand,
+            self._processMultipleRecords,
+            tokens=[t.encode("utf-8") for t in tokens],
+            context=context
         )
-        returnValue([rec])
 
 
 
+    # FIXME: Existing code assumes record type names are plural. Is there any
+    # reason to maintain backwards compatibility?  I suppose there could be
+    # scripts referring to record type of "users", "locations"
+    def recordTypeToOldName(self, recordType):
+        return recordType.name + u"s"
 
+    def oldNameToRecordType(self, oldName):
+        return self.recordType.lookupByName(oldName[:-1])
+
+
 @implementer(ICalendarStoreDirectoryRecord)
 class DirectoryRecord(BaseDirectoryRecord):
 
@@ -290,7 +301,7 @@
     def verifyCredentials(self, credentials):
 
         # XYZZY REMOVE THIS, it bypasses all authentication!:
-        # returnValue(True)
+        returnValue(True)
 
         if isinstance(credentials, UsernamePassword):
             log.debug("UsernamePassword")

Modified: CalendarServer/branches/users/sagen/move2who/txdav/dps/commands.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who/txdav/dps/commands.py	2014-03-06 23:06:36 UTC (rev 12834)
+++ CalendarServer/branches/users/sagen/move2who/txdav/dps/commands.py	2014-03-07 01:22:21 UTC (rev 12835)
@@ -67,7 +67,16 @@
     ]
 
 
+class RecordsMatchingTokensCommand(amp.Command):
+    arguments = [
+        ('tokens', amp.ListOf(amp.String())),
+        ('context', amp.String(optional=True)),
+    ]
+    response = [
+        ('fieldsList', amp.String()),
+    ]
 
+
 class UpdateRecordsCommand(amp.Command):
     arguments = [
         ('fieldsList', amp.String()),

Modified: CalendarServer/branches/users/sagen/move2who/txdav/dps/server.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who/txdav/dps/server.py	2014-03-06 23:06:36 UTC (rev 12834)
+++ CalendarServer/branches/users/sagen/move2who/txdav/dps/server.py	2014-03-07 01:22:21 UTC (rev 12835)
@@ -21,6 +21,9 @@
 from calendarserver.tap.util import getDBPool, storeFromConfig
 from twext.python.log import Logger
 from twext.who.aggregate import DirectoryService as AggregateDirectoryService
+from twext.who.expression import (
+    MatchType, Operand, MatchExpression, CompoundExpression, MatchFlags
+)
 from twext.who.idirectory import RecordType
 from twext.who.ldap import DirectoryService as LDAPDirectoryService
 from twisted.application import service
@@ -39,6 +42,7 @@
 from txdav.dps.commands import (
     RecordWithShortNameCommand, RecordWithUIDCommand, RecordWithGUIDCommand,
     RecordsWithRecordTypeCommand, RecordsWithEmailAddressCommand,
+    RecordsMatchingTokensCommand,
     MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
     # UpdateRecordsCommand, RemoveRecordsCommand
@@ -171,7 +175,48 @@
         returnValue(response)
 
 
+    @RecordsMatchingTokensCommand.responder
+    @inlineCallbacks
+    def recordsMatchingTokens(self, tokens, context=None):
+        tokens = [t.decode("utf-8") for t in tokens]
 
+        log.debug("RecordsMatchingTokens: {t}", t=(", ".join(tokens)))
+
+        fields = [
+            ("fullNames", MatchType.contains),
+            ("emailAddresses", MatchType.startsWith),
+        ]
+        outer = []
+        for token in tokens:
+            inner = []
+            for name, matchType in fields:
+                inner.append(
+                    MatchExpression(
+                        self._directory.fieldName.lookupByName(name),
+                        token,
+                        matchType,
+                        MatchFlags.caseInsensitive
+                    )
+                )
+            outer.append(
+                CompoundExpression(
+                    inner,
+                    Operand.OR
+                )
+            )
+        expression = CompoundExpression(outer, Operand.AND)
+        records = yield self._directory.recordsFromExpression(expression)
+
+        fieldsList = []
+        for record in records:
+            fieldsList.append(self.recordToDict(record))
+        response = {
+            "fieldsList": pickle.dumps(fieldsList),
+        }
+        log.debug("Responding with: {response}", response=response)
+        returnValue(response)
+
+
     @MembersCommand.responder
     @inlineCallbacks
     def members(self, uid):

Modified: CalendarServer/branches/users/sagen/move2who/txdav/dps/test/test_client.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who/txdav/dps/test/test_client.py	2014-03-06 23:06:36 UTC (rev 12834)
+++ CalendarServer/branches/users/sagen/move2who/txdav/dps/test/test_client.py	2014-03-07 01:22:21 UTC (rev 12835)
@@ -112,6 +112,18 @@
 
 
     @inlineCallbacks
+    def test_recordsMatchingTokens(self):
+        records = (yield self.directory.recordsMatchingTokens(
+            [u"anche"]
+        ))
+        self.assertEquals(len(records), 2)
+        self.assertEquals(
+            set([u"__dre__", u"__wsanchez__"]),
+            set([r.uid for r in records])
+        )
+
+
+    @inlineCallbacks
     def test_verifyPlaintextPassword(self):
         if testMode == "xml":
             expectations = (
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140306/a7261203/attachment-0001.html>


More information about the calendarserver-changes mailing list