[CalendarServer-changes] [3019] CalendarServer/branches/users/sagen/principal-property-search-3017

source_changes at macosforge.org source_changes at macosforge.org
Thu Sep 18 15:52:37 PDT 2008


Revision: 3019
          http://trac.macosforge.org/projects/calendarserver/changeset/3019
Author:   sagen at apple.com
Date:     2008-09-18 15:52:36 -0700 (Thu, 18 Sep 2008)
Log Message:
-----------
Merging forward

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/principal-property-search-3017/conf/accounts-test.xml
    CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch
    CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/customxml.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/aggregate.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/apache.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/idirectory.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/principal.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sqldb.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sudo.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlaccountsparser.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlfile.py
    CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/resource.py

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/conf/accounts-test.xml
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/conf/accounts-test.xml	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/conf/accounts-test.xml	2008-09-18 22:52:36 UTC (rev 3019)
@@ -24,12 +24,16 @@
     <guid>admin</guid>
     <password>admin</password>
     <name>Super User</name>
+    <first-name>Super</first-name>
+    <last-name>User</last-name>
   </user>
   <user>
     <uid>apprentice</uid>
     <guid>apprentice</guid>
     <password>apprentice</password>
     <name>Apprentice Super User</name>
+    <first-name>Apprentice</first-name>
+    <last-name>Super User</last-name>
   </user>
   <user repeat="99">
     <uid>user%02d</uid>
@@ -37,6 +41,8 @@
     <password>user%02d</password>
     <name>User %02d</name>
     <cuaddr>mailto:user%02d at example.com</cuaddr>
+    <first-name>User</first-name>
+    <last-name>%02d</last-name>
   </user>
   <user repeat="10">
     <uid>public%02d</uid>
@@ -44,6 +50,8 @@
     <password>public%02d</password>
     <name>Public %02d</name>
     <cuaddr>mailto:public%02d at example.com</cuaddr>
+    <first-name>Public</first-name>
+    <last-name>%02d</last-name>
   </user>
   <location repeat="10">
     <uid>location%02d</uid>

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.element.rfc3744.patch	2008-09-18 22:52:36 UTC (rev 3019)
@@ -81,3 +81,11 @@
  
          # This element can be empty when uses in supported-report-set
          if not len(self.children):
+@@ -705,6 +714,7 @@
+         (dav_namespace, "prop"                             ): (0, 1),
+         (dav_namespace, "apply-to-principal-collection-set"): (0, 1),
+     }
++    allowed_attributes = { "test": False }
+ 
+ class PropertySearch (WebDAVElement):
+     """

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch	2008-09-18 22:52:36 UTC (rev 3019)
@@ -2,19 +2,263 @@
 ===================================================================
 --- twisted/web2/dav/method/report_principal_property_search.py	(revision 19773)
 +++ twisted/web2/dav/method/report_principal_property_search.py	(working copy)
-@@ -127,13 +127,8 @@
+@@ -51,12 +51,18 @@
+         raise ValueError("%s expected as root element, not %s."
+                          % (davxml.PrincipalPropertySearch.sname(), principal_property_search.sname()))
+ 
++    # Should we AND (the default) or OR (if test="anyof")?
++    testMode = principal_property_search.attributes.get("test", "allof")
++    if testMode not in ("allof", "anyof"):
++        raise ValueError("Unknown value for test attribute: %s" % (testMode,))
++    operand = "and" if testMode == "allof" else "or"
++
+     # Only handle Depth: 0
+     depth = request.headers.getHeader("depth", "0")
+     if depth != "0":
+         log.err("Error in prinicpal-property-search REPORT, Depth set to %s" % (depth,))
+         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))
+-    
++
+     # Get a single DAV:prop element from the REPORT request body
+     propertiesForResource = None
+     propElement = None
+@@ -93,73 +99,155 @@
+         else:
+             return False
+         
+-    def propertySearch(resource, request):
++    def propertySearch(resource, request, operand):
+         """
+         Test the resource to see if it contains properties matching the
+         property-search specification in this report.
+         @param resource: the L{DAVFile} for the resource to test.
+         @param request:  the current request.
++        @param operand:  "and" or "or"
+         @return:         True if the resource has matching properties, False otherwise.
+         """
+-        for props, match in propertySearches:
+-            # Test each property
+-            for prop in props:
+-                try:
+-                    propvalue = waitForDeferred(resource.readProperty(prop.qname(), request))
+-                    yield propvalue
+-                    propvalue = propvalue.getResult()
+-                    if propvalue and not nodeMatch(propvalue, match):
++
++        if operand == "and":
++            for props, match in propertySearches:
++                # Test each property
++                for prop in props:
++                    try:
++                        propvalue = waitForDeferred(resource.readProperty(prop.qname(), request))
++                        yield propvalue
++                        propvalue = propvalue.getResult()
++                        if propvalue:
++                            if not nodeMatch(propvalue, match):
++                                yield False
++                                return
++                    except HTTPError:
++                        # No property => no match
+                         yield False
+                         return
+-                except HTTPError:
+-                    # No property => no match
+-                    yield False
+-                    return
+-        
+-        yield True
++            # we hit on every property
++            yield True
+ 
++        else: # "or"
++            for props, match in propertySearches:
++                # Test each property
++                for prop in props:
++                    try:
++                        propvalue = waitForDeferred(resource.readProperty(prop.qname(), request))
++                        yield propvalue
++                        propvalue = propvalue.getResult()
++                        if propvalue:
++                            if nodeMatch(propvalue, match):
++                                yield True
++                                return
++                    except HTTPError:
++                        # No property
++                        pass
++            # we didn't hit any
++            yield False
++
+     propertySearch = deferredGenerator(propertySearch)
+ 
++
+     # Run report
+     try:
+-        resources = []
+-        responses = []
++
++        # See if we can take advantage of the directory
++        fields = []
++        nonDirectorySearches = []
++        for props, match in propertySearches:
++            nonDirectoryProps = []
++            for prop in props:
++                try:
++                    fieldName = self.propertyToField(prop)
++                except AttributeError:
++                    fieldName = None
++                if fieldName:
++                    fields.append((fieldName, match))
++                else:
++                    nonDirectoryProps.append(prop)
++            if nonDirectoryProps:
++                nonDirectorySearches.append((nonDirectoryProps, match))
++
++        matchingResources = []
          matchcount = 0
  
-         if applyTo:
+-        if applyTo:
 -            # Get the principal collection set
 -            pset = waitForDeferred(self.principalCollections(request))
 -            yield pset
 -            pset = pset.getResult()
--
++        if (
++            (operand == "or" and nonDirectorySearches) or
++            (operand == "and" and nonDirectorySearches and not fields)
++        ):
++            # These are the situations in which we need to iterate all the
++            # resources
++            resources = []
+ 
 -            for phref in pset:
 -                uri = str(phref)
-+            for principalCollection in self.principalCollections():
-+                uri = principalCollection.principalCollectionURL()
-                 resource = waitForDeferred(request.locateResource(uri))
-                 yield resource
-                 resource = resource.getResult()
+-                resource = waitForDeferred(request.locateResource(uri))
+-                yield resource
+-                resource = resource.getResult()
+-                if resource:
+-                    resources.append((resource, uri))
+-        else:
+-            resources.append((self, request.uri))
++            if applyTo:
++                for principalCollection in self.principalCollections():
++                    uri = principalCollection.principalCollectionURL()
++                    resource = waitForDeferred(request.locateResource(uri))
++                    yield resource
++                    resource = resource.getResult()
++                    if resource:
++                        resources.append((resource, uri))
++            else:
++                resources.append((self, request.uri))
+ 
+-        # Loop over all collections and principal resources within
+-        for resource, ruri in resources:
++            # Loop over all collections and principal resources within
++            for resource, ruri in resources:
+ 
+-            # Do some optimisation of access control calculation by determining any inherited ACLs outside of
+-            # the child resource loop and supply those to the checkPrivileges on each child.
+-            filteredaces = waitForDeferred(resource.inheritedACEsforChildren(request))
+-            yield filteredaces
+-            filteredaces = filteredaces.getResult()
++                # Do some optimisation of access control calculation by determining any inherited ACLs outside of
++                # the child resource loop and supply those to the checkPrivileges on each child.
++                filteredaces = waitForDeferred(resource.inheritedACEsforChildren(request))
++                yield filteredaces
++                filteredaces = filteredaces.getResult()
+ 
+-            children = []
+-            d = waitForDeferred(resource.findChildren("infinity", request, lambda x, y: children.append((x,y)),
+-                                                      privileges=(davxml.Read(),), inherited_aces=filteredaces))
+-            yield d
+-            d.getResult()
++                children = []
++                d = waitForDeferred(resource.findChildren("infinity", request,
++                    lambda x, y: children.append((x,y)),
++                    privileges=(davxml.Read(),), inherited_aces=filteredaces))
++                yield d
++                d.getResult()
+ 
+-            for child, uri in children:
+-                if isPrincipalResource(child):
+-                    d = waitForDeferred(propertySearch(child, request))
++                for child, uri in children:
++                    if isPrincipalResource(child):
++                        d = waitForDeferred(propertySearch(child, request,
++                            operand))
++                        yield d
++                        d = d.getResult()
++                        if d:
++                            # Check size of results is within limit
++                            matchcount += 1
++                            if matchcount > max_number_of_matches:
++                                raise NumberOfMatchesWithinLimits
++
++                            matchingResources.append(child)
++
++
++        elif fields: # search the directory
++            try:
++                recordType = self.recordType
++            except AttributeError:
++                recordType = None
++
++            for record in self.directory.recordsMatchingFields(fields,
++                operand=operand, recordType=recordType):
++
++                resource = self.principalForRecord(record)
++                url = resource.url()
++
++                if not nonDirectorySearches:
++                    # We've determined this is a matching resource
++
++                    matchcount += 1
++                    if matchcount > max_number_of_matches:
++                        raise NumberOfMatchesWithinLimits
++                    matchingResources.append(resource)
++
++                elif operand == "and":
++                    # Further narrowing down needs to take place by examining
++                    # the resource's DAV properties
++
++                    d = waitForDeferred(propertySearch(resource, request,
++                        operand))
+                     yield d
+                     d = d.getResult()
+                     if d:
+@@ -167,18 +255,26 @@
+                         matchcount += 1
+                         if matchcount > max_number_of_matches:
+                             raise NumberOfMatchesWithinLimits
+-    
+-                        d = waitForDeferred(prop_common.responseForHref(
+-                            request,
+-                            responses,
+-                            davxml.HRef.fromString(uri),
+-                            child,
+-                            propertiesForResource,
+-                            propElement
+-                        ))
+-                        yield d
+-                        d.getResult()
++                        matchingResources.append(resource)
+ 
++
++
++        # Generate the response
++        responses = []
++        for resource in matchingResources:
++            url = resource.url()
++            d = waitForDeferred(prop_common.responseForHref(
++                request,
++                responses,
++                davxml.HRef.fromString(url),
++                resource,
++                propertiesForResource,
++                propElement
++            ))
++            yield d
++            d.getResult()
++
++
+     except NumberOfMatchesWithinLimits:
+         log.err("Too many matching components in prinicpal-property-search report")
+         raise HTTPError(ErrorResponse(

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/customxml.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/customxml.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -290,6 +290,30 @@
     protected = True
     hidden = True
 
+class FirstNameProperty (davxml.WebDAVTextElement):
+    """
+    A property representing first name of a principal
+    """
+    namespace = calendarserver_namespace
+    name = "first-name"
+    protected = True
+
+class LastNameProperty (davxml.WebDAVTextElement):
+    """
+    A property representing last name of a principal
+    """
+    namespace = calendarserver_namespace
+    name = "last-name"
+    protected = True
+
+class EMailProperty (davxml.WebDAVTextElement):
+    """
+    A property representing email address of a principal
+    """
+    namespace = calendarserver_namespace
+    name = "email-address"
+    protected = True
+
 class IScheduleInbox (davxml.WebDAVEmptyElement):
     """
     Denotes the resourcetype of a iSchedule Inbox.

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/aggregate.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/aggregate.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -103,6 +103,19 @@
     def recordWithCalendarUserAddress(self, address):
         return self._queryAll("recordWithCalendarUserAddress", address)
 
+    def recordsMatchingFields(self, fields, caseInsensitive=True, operand="or",
+        recordType=None):
+        if recordType:
+            services = (self.serviceForRecordType(recordType),)
+        else:
+            services = set(self._recordTypes.values())
+
+        for service in services:
+            for record in service.recordsMatchingFields(fields,
+                caseInsensitive=caseInsensitive, operand=operand,
+                recordType=recordType):
+                    yield record
+
     def serviceForRecordType(self, recordType):
         try:
             return self._recordTypes[recordType]

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/apache.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/apache.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/apache.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -129,6 +129,9 @@
             guid                  = None,
             shortName             = shortName,
             fullName              = None,
+            firstName             = None,
+            lastName              = None,
+            emailAddress          = None,
             calendarUserAddresses = set(),
             autoSchedule          = False,
         )

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/appleopendirectory.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/appleopendirectory.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -460,6 +460,100 @@
 
         return record
 
+
+    _ODFields = {
+        'fullName' : dsattributes.kDS1AttrDistinguishedName,
+        'firstName' : dsattributes.kDS1AttrFirstName,
+        'lastName' : dsattributes.kDS1AttrLastName,
+        'emailAddress' : dsattributes.kDSNAttrEMailAddress,
+    }
+
+    _toODRecordTypes = {
+        DirectoryService.recordType_users :
+            dsattributes.kDSStdRecordTypeUsers,
+        DirectoryService.recordType_locations :
+            dsattributes.kDSStdRecordTypePlaces,
+        DirectoryService.recordType_groups :
+            dsattributes.kDSStdRecordTypeGroups,
+        DirectoryService.recordType_resources :
+            dsattributes.kDSStdRecordTypeResources,
+    }
+
+    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
+
+    def recordsMatchingFields(self, fields, caseInsensitive=True, operand="or",
+        recordType=None):
+
+        comparison = dsattributes.eDSStartsWith
+        operand = (dsquery.expression.OR if operand == "or"
+            else dsquery.expression.AND)
+
+        expressions = []
+        for field, value in fields:
+            if field in self._ODFields:
+                ODField = self._ODFields[field]
+                expressions.append(dsquery.match(ODField, value, comparison))
+
+
+        if recordType is None:
+            recordTypes = self._toODRecordTypes.values()
+        else:
+            recordTypes = (self._toODRecordTypes[recordType],)
+
+        for recordType in recordTypes:
+
+            try:
+                self.log_info("Calling OD: %s %s %s" % (recordType, operand,
+                    fields))
+                results = opendirectory.queryRecordsWithAttributes(
+                    self.directory,
+                    dsquery.expression(operand, expressions).generate(),
+                    caseInsensitive,
+                    recordType,
+                    [
+                        dsattributes.kDS1AttrGeneratedUID,
+                        dsattributes.kDS1AttrFirstName,
+                        dsattributes.kDS1AttrLastName,
+                        dsattributes.kDSNAttrEMailAddress,
+                        dsattributes.kDS1AttrDistinguishedName,
+                        dsattributes.kDSNAttrMetaNodeLocation,
+                    ]
+                )
+                self.log_info("Got back %d records from OD" % (len(results),))
+                for key, val in results.iteritems():
+                    self.log_debug("OD result: %s %s" % (key, val))
+                    try:
+                        calendarUserAddresses = set()
+                        enabledForCalendaring = False
+                        if val.has_key(dsattributes.kDSNAttrEMailAddress):
+                            enabledForCalendaring = True
+                            calendarUserAddresses.add(val[dsattributes.kDSNAttrEMailAddress])
+                        rec = OpenDirectoryRecord(
+                            service = self,
+                            recordType = self._fromODRecordTypes[recordType],
+                            guid = val[dsattributes.kDS1AttrGeneratedUID],
+                            nodeName = val[dsattributes.kDSNAttrMetaNodeLocation],
+                            shortName = key,
+                            fullName = val.get(dsattributes.kDS1AttrDistinguishedName, ""),
+                            firstName = val.get(dsattributes.kDS1AttrFirstName, ""),
+                            lastName = val.get(dsattributes.kDS1AttrLastName, ""),
+                            emailAddress = val.get(dsattributes.kDSNAttrEMailAddress, ""),
+                            calendarUserAddresses = calendarUserAddresses,
+                            autoSchedule = False,
+                            enabledForCalendaring = enabledForCalendaring,
+                            memberGUIDs = (),
+                            proxyGUIDs = (),
+                            readOnlyProxyGUIDs = (),
+                        )
+                        yield rec
+                    except Exception, e:
+                        self.log_error("Failed to convert OD result into record: %s %s" % (val, e))
+                        raise
+
+            except Exception, e:
+                self.log_error("OD search failed: %s" % (e,))
+                raise
+
     def reloadCache(self, recordType, shortName=None, guid=None):
         if shortName:
             self.log_info("Faulting record %s into %s record cache" % (shortName, recordType))
@@ -467,7 +561,7 @@
             self.log_info("Reloading %s record cache" % (recordType,))
 
         results = self._queryDirectory(recordType, shortName=shortName, guid=guid)
-        
+
         if shortName is None and guid is None:
             records = {}
             guids   = {}
@@ -533,6 +627,9 @@
             # Now get useful record info.
             recordGUID     = value.get(dsattributes.kDS1AttrGeneratedUID)
             recordFullName = value.get(dsattributes.kDS1AttrDistinguishedName)
+            recordFirstName = value.get(dsattributes.kDS1AttrFirstName)
+            recordLastName = value.get(dsattributes.kDS1AttrLastName)
+            recordEmailAddress = value.get(dsattributes.kDSNAttrEMailAddress)
             recordNodeName = value.get(dsattributes.kDSNAttrMetaNodeLocation)
 
             if not recordGUID:
@@ -581,6 +678,9 @@
                 nodeName              = recordNodeName,
                 shortName             = recordShortName,
                 fullName              = recordFullName,
+                firstName             = recordFirstName,
+                lastName              = recordLastName,
+                emailAddress          = recordEmailAddress,
                 calendarUserAddresses = calendarUserAddresses,
                 autoSchedule          = autoSchedule,
                 enabledForCalendaring = enabledForCalendaring,
@@ -665,6 +765,8 @@
         attrs = [
             dsattributes.kDS1AttrGeneratedUID,
             dsattributes.kDS1AttrDistinguishedName,
+            dsattributes.kDS1AttrFirstName,
+            dsattributes.kDS1AttrLastName,
             dsattributes.kDSNAttrEMailAddress,
             dsattributes.kDSNAttrServicesLocator,
             dsattributes.kDSNAttrMetaNodeLocation,
@@ -827,6 +929,7 @@
     """
     def __init__(
         self, service, recordType, guid, nodeName, shortName, fullName,
+        firstName, lastName, emailAddress,
         calendarUserAddresses, autoSchedule, enabledForCalendaring,
         memberGUIDs, proxyGUIDs, readOnlyProxyGUIDs,
     ):
@@ -836,6 +939,9 @@
             guid                  = guid,
             shortName             = shortName,
             fullName              = fullName,
+            firstName             = firstName,
+            lastName              = lastName,
+            emailAddress          = emailAddress,
             calendarUserAddresses = calendarUserAddresses,
             autoSchedule          = autoSchedule,
             enabledForCalendaring = enabledForCalendaring,

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/directory.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/directory.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -138,7 +138,61 @@
             for record in self.listRecords(recordType):
                 yield record
 
+    def recordsMatchingFields(self, fields, caseInsensitive=True, operand="or",
+        recordType=None):
+        # Default, bruteforce method; override with one optimized for each
+        # service
 
+        if recordType is None:
+            recordTypes = (
+                DirectoryService.recordType_users,
+                DirectoryService.recordType_groups,
+                DirectoryService.recordType_locations,
+                DirectoryService.recordType_resources,
+            )
+        else:
+            recordTypes = (recordType,)
+
+        def fieldMatches(fieldValue, value):
+            if caseInsensitive:
+                return fieldValue.lower().startswith(value.lower())
+            else:
+                return fieldValue.startswith(value)
+
+        def recordMatches(record):
+            if operand == "and":
+                for fieldName, value in fields:
+                    try:
+                        fieldValue = getattr(record, fieldName)
+                        if not fieldMatches(fieldValue, value):
+                            return False
+                    except AttributeError:
+                        # No property => no match
+                        return False
+                # we hit on every property
+                return True
+            else: # "or"
+                for fieldName, value in fields:
+                    try:
+                        fieldValue = getattr(record, fieldName)
+                        if fieldMatches(fieldValue, value):
+                            return True
+                    except AttributeError:
+                        # No value
+                        pass
+                # we didn't hit any
+                return False
+
+        try:
+            for recordType in recordTypes:
+                for record in self.listRecords(recordType):
+                    if recordMatches(record):
+                        yield record
+        except UnknownRecordTypeError:
+            # Skip this service since it doesn't understand this record type
+            pass
+
+
 class DirectoryRecord(LoggingMixIn):
     implements(IDirectoryRecord)
 
@@ -155,6 +209,7 @@
 
     def __init__(
         self, service, recordType, guid, shortName, fullName,
+        firstName, lastName, emailAddress,
         calendarUserAddresses, autoSchedule, enabledForCalendaring=True,
     ):
         assert service.realmName is not None
@@ -174,6 +229,9 @@
         self.guid                  = guid
         self.shortName             = shortName
         self.fullName              = fullName
+        self.firstName             = firstName
+        self.lastName              = lastName
+        self.emailAddress          = emailAddress
         self.enabledForCalendaring = enabledForCalendaring
         self.calendarUserAddresses = calendarUserAddresses
         self.autoSchedule          = autoSchedule

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/idirectory.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/idirectory.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -72,6 +72,13 @@
             addresses.
         """
 
+    def recordsMatchingFields(fields):
+        """
+        @return: a sequence of L{IDirectoryRecord}s which match the given
+            fields.
+        """
+
+
 class IDirectoryRecord(Interface):
     """
     Directory Record
@@ -81,6 +88,9 @@
     guid                  = Attribute("The GUID of this record.")
     shortName             = Attribute("The name of this record.")
     fullName              = Attribute("The full name of this record.")
+    firstName             = Attribute("The first name of this record.")
+    lastName              = Attribute("The last name of this record.")
+    emailAddress          = Attribute("The email address of this record.")
     calendarUserAddresses = Attribute("A set of calendar user addresses for this record.")
     autoSchedule          = Attribute("Principal identified by this record should automatically accept/deny meetings.")
     enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/principal.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/principal.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -125,6 +125,26 @@
     def principalForCalendarUserAddress(self, address):
         raise NotImplementedError("Subclass must implement principalForCalendarUserAddress()")
 
+    ##
+    # DAV-property-to-record-field mapping
+    ##
+
+    _cs_ns = "http://calendarserver.org/ns/"
+    _fieldMap = {
+        ("DAV:" , "displayname") : "fullName",
+        (_cs_ns, "first-name") : "firstName",
+        (_cs_ns, "last-name") : "lastName",
+        (_cs_ns, "email-address") : "emailAddress",
+    }
+
+    def propertyToField(self, property):
+        """
+        If property is a DAV property that maps to a directory field, return
+        that field's name, otherwise return None
+        """
+        return self._fieldMap.get(property.qname(), None)
+
+
 class DirectoryPrincipalProvisioningResource (DirectoryProvisioningResource):
     """
     Collection resource which provisions directory principals as its children.
@@ -212,6 +232,7 @@
 
         return None
 
+
     ##
     # Static
     ##
@@ -236,6 +257,7 @@
     def principalCollections(self):
         return (self,)
 
+
 class DirectoryPrincipalTypeProvisioningResource (DirectoryProvisioningResource):
     """
     Collection resource which provisions directory principals of a
@@ -289,6 +311,7 @@
     def principalCollections(self):
         return self.parent.principalCollections()
 
+
 class DirectoryPrincipalUIDProvisioningResource (DirectoryProvisioningResource):
     """
     Collection resource which provisions directory principals indexed
@@ -437,6 +460,9 @@
             """Record type: %s\n"""            % (self.record.recordType,),
             """Short name: %s\n"""             % (self.record.shortName,),
             """Full name: %s\n"""              % (self.record.fullName,),
+            """First name: %s\n"""             % (self.record.firstName,),
+            """Last name: %s\n"""              % (self.record.lastName,),
+            """Email address: %s\n"""          % (self.record.emailAddress,),
             """Principal UID: %s\n"""          % (self.principalUID(),),
             """Principal URL: %s\n"""          % (format_link(self.principalURL()),),
             """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),
@@ -597,6 +623,9 @@
             """Record type: %s\n"""            % (self.record.recordType,),
             """Short name: %s\n"""             % (self.record.shortName,),
             """Full name: %s\n"""              % (self.record.fullName,),
+            """First name: %s\n"""             % (self.record.firstName,),
+            """Last name: %s\n"""              % (self.record.lastName,),
+            """Email address: %s\n"""          % (self.record.emailAddress,),
             """Principal UID: %s\n"""          % (self.principalUID(),),
             """Principal URL: %s\n"""          % (format_link(self.principalURL()),),
             """\nAlternate URIs:\n"""          , format_list(format_link(u) for u in self.alternateURIs()),

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sqldb.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sqldb.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sqldb.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -350,6 +350,9 @@
             guid                  = guid,
             shortName             = shortName,
             fullName              = name,
+            firstName             = None,
+            lastName              = None,
+            emailAddress          = None,
             calendarUserAddresses = calendarUserAddresses,
             autoSchedule          = autoSchedule,
         )

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sudo.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sudo.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/sudo.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -132,6 +132,9 @@
             guid=None,
             shortName=shortName,
             fullName=shortName,
+            firstName="",
+            lastName="",
+            emailAddress="",
             calendarUserAddresses=set(),
             autoSchedule=False,
             enabledForCalendaring=False)

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/test/test_opendirectory.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/test/test_opendirectory.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -67,6 +67,9 @@
                 nodeName              = "/LDAPv2/127.0.0.1",
                 shortName             = "user",
                 fullName              = "Some user",
+                firstName             = "Some",
+                lastName              = "User",
+                emailAddress          = "someuser at example.com",
                 calendarUserAddresses = set(("mailtoguid at example.com",)),
                 autoSchedule          = False,
                 enabledForCalendaring = True,

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlaccountsparser.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlaccountsparser.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -43,6 +43,9 @@
 ELEMENT_GUID              = "guid"
 ELEMENT_PASSWORD          = "password"
 ELEMENT_NAME              = "name"
+ELEMENT_FIRST_NAME        = "first-name"
+ELEMENT_LAST_NAME         = "last-name"
+ELEMENT_EMAIL_ADDRESS     = "email-address"
 ELEMENT_MEMBERS           = "members"
 ELEMENT_MEMBER            = "member"
 ELEMENT_CUADDR            = "cuaddr"
@@ -163,6 +166,9 @@
         self.guid = None
         self.password = None
         self.name = None
+        self.firstName = None
+        self.lastName = None
+        self.emailAddress = None
         self.members = set()
         self.groups = set()
         self.calendarUserAddresses = set()
@@ -195,6 +201,18 @@
             name = self.name % ctr
         else:
             name = self.name
+        if self.firstName and self.firstName.find("%") != -1:
+            firstName = self.firstName % ctr
+        else:
+            firstName = self.firstName
+        if self.lastName and self.lastName.find("%") != -1:
+            lastName = self.lastName % ctr
+        else:
+            lastName = self.lastName
+        if self.emailAddress and self.emailAddress.find("%") != -1:
+            emailAddress = self.emailAddress % ctr
+        else:
+            emailAddress = self.emailAddress
         calendarUserAddresses = set()
         for cuaddr in self.calendarUserAddresses:
             if cuaddr.find("%") != -1:
@@ -207,6 +225,9 @@
         result.guid = guid
         result.password = password
         result.name = name
+        result.firstName = firstName
+        result.lastName = lastName
+        result.emailAddress = emailAddress
         result.members = self.members
         result.calendarUserAddresses = calendarUserAddresses
         result.autoSchedule = self.autoSchedule
@@ -237,6 +258,15 @@
             elif child_name == ELEMENT_NAME:
                 if child.firstChild is not None:
                     self.name = child.firstChild.data.encode("utf-8")
+            elif child_name == ELEMENT_FIRST_NAME:
+                if child.firstChild is not None:
+                    self.firstName = child.firstChild.data.encode("utf-8")
+            elif child_name == ELEMENT_LAST_NAME:
+                if child.firstChild is not None:
+                    self.lastName = child.firstChild.data.encode("utf-8")
+            elif child_name == ELEMENT_EMAIL_ADDRESS:
+                if child.firstChild is not None:
+                    self.emailAddress = child.firstChild.data.encode("utf-8")
             elif child_name == ELEMENT_MEMBERS:
                 self._parseMembers(child, self.members)
             elif child_name == ELEMENT_CUADDR:

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlfile.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/directory/xmlfile.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -108,6 +108,9 @@
             guid                  = xmlPrincipal.guid,
             shortName             = shortName,
             fullName              = xmlPrincipal.name,
+            firstName             = xmlPrincipal.firstName,
+            lastName              = xmlPrincipal.lastName,
+            emailAddress          = xmlPrincipal.emailAddress,
             calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
             autoSchedule          = xmlPrincipal.autoSchedule,
             enabledForCalendaring = xmlPrincipal.enabledForCalendaring,

Modified: CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/resource.py	2008-09-18 22:50:21 UTC (rev 3018)
+++ CalendarServer/branches/users/sagen/principal-property-search-3017/twistedcaldav/resource.py	2008-09-18 22:52:36 UTC (rev 3019)
@@ -705,6 +705,9 @@
         (caldav_namespace, "calendar-user-address-set"),
         (caldav_namespace, "schedule-inbox-URL"       ),
         (caldav_namespace, "schedule-outbox-URL"      ),
+        (calendarserver_namespace, "first-name"       ),
+        (calendarserver_namespace, "last-name"        ),
+        (calendarserver_namespace, "email-address"    ),
     )
 
     @classmethod
@@ -760,6 +763,28 @@
                     else:
                         return customxml.DropBoxHomeURL(davxml.HRef(url))
 
+                if name == "first-name":
+                    firstName = self.record.firstName
+                    if firstName:
+                        return customxml.FirstNameProperty(firstName)
+                    else:
+                        return None
+
+                if name == "last-name":
+                    lastName = self.record.lastName
+                    if lastName:
+                        return customxml.LastNameProperty(lastName)
+                    else:
+                        return None
+
+                if name == "email-address":
+                    emailAddress = self.record.emailAddress
+                    if emailAddress:
+                        return customxml.EMailProperty(emailAddress)
+                    else:
+                        return None
+
+
             return super(CalendarPrincipalResource, self).readProperty(property, request)
 
         return maybeDeferred(defer)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080918/fde917ef/attachment-0001.html 


More information about the calendarserver-changes mailing list