[CalendarServer-changes] [3138] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri Oct 10 10:34:51 PDT 2008


Revision: 3138
          http://trac.macosforge.org/projects/calendarserver/changeset/3138
Author:   sagen at apple.com
Date:     2008-10-10 10:34:50 -0700 (Fri, 10 Oct 2008)
Log Message:
-----------
- Adds calendar-user-type DAV property
- Adds ability to principal-property-search by CUType
- principal-search-property-set is now generated from the properties searched via directory
- Fixes twistedcaldav.scheduling.test.test_itip.iTIPGenerator.test_request

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/caldavxml.py
    CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/root.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
    CalendarServer/trunk/twistedcaldav/static.py

Removed Paths:
-------------
    CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch

Deleted: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.method.report_principal_property_search.patch	2008-10-10 17:34:50 UTC (rev 3138)
@@ -1,310 +0,0 @@
-Index: twisted/web2/dav/method/report_principal_property_search.py
-===================================================================
---- twisted/web2/dav/method/report_principal_property_search.py	(revision 19773)
-+++ twisted/web2/dav/method/report_principal_property_search.py	(working copy)
-@@ -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
-@@ -71,10 +77,22 @@
-         elif child.qname() == (dav_namespace, "property-search"):
-             props = child.childOfType(davxml.PropertyContainer)
-             props.removeWhitespaceNodes()
-+
-             match = child.childOfType(davxml.Match)
--            propertySearches.append((props.children, str(match).lower()))
-+            caseless = match.attributes.get("caseless", "yes")
-+            if caseless not in ("yes", "no"):
-+                raise ValueError("Unknown value for caseless attribute: %s" %
-+                    (caseless,))
-+            caseless = True if caseless == "yes" else False
-+            matchType = match.attributes.get("match-type", "contains")
-+            if matchType not in ("starts-with", "contains"):
-+                raise ValueError("Unknown value for match-type attribute: %s" %
-+                    (matchType,))
-+
-+            propertySearches.append((props.children, str(match),
-+                caseless, matchType))
-     
--    def nodeMatch(node, match):
-+    def nodeMatch(node, match, caseless, matchType):
-         """
-         See if the content of the supplied node matches the supplied text.
-         Try to follow the matching guidance in rfc3744 section 9.4.1.
-@@ -85,81 +103,174 @@
-         node.removeWhitespaceNodes()
-         for child in node.children:
-             if isinstance(child, davxml.PCDATAElement):
--                comp = str(child).lower()
--                if comp.find(match) != -1:
-+                if caseless:
-+                    comp = str(child).lower()
-+                    match = match.lower()
-+                else:
-+                    comp = str(child)
-+
-+                if matchType == "starts-with":
-+                    if comp.find(match) == 0:
-+                        return True
-+                elif comp.find(match) != -1:
-                     return True
-+
-             else:
-                 return nodeMatch(child, match)
-         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, caseless, matchType 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, caseless,
-+                                matchType):
-+                                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, caseless, matchType 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, caseless, matchType):
-+                                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, caseless, matchType in propertySearches:
-+            nonDirectoryProps = []
-+            for prop in props:
-+                try:
-+                    fieldName = self.propertyToField(prop)
-+                except AttributeError:
-+                    fieldName = None
-+                if fieldName:
-+                    fields.append((fieldName, match, caseless, matchType))
-+                else:
-+                    nonDirectoryProps.append(prop)
-+            if nonDirectoryProps:
-+                nonDirectorySearches.append((nonDirectoryProps, match,
-+                    caseless, matchType))
-+
-+        matchingResources = []
-         matchcount = 0
- 
--        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)
--                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 +278,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/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -1733,6 +1733,13 @@
     """
     name = "schedule-unprocessed"
 
+class CalendarUserType (CalDAVTextElement):
+    """
+    The CALDAV:calendar-user-type property from section 9.2.4 of caldav-sched-05
+    """
+    name = "calendar-user-type"
+    protected = True
+
 ##
 # Extensions to davxml.ResourceType
 ##

Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -359,10 +359,13 @@
                 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))
-                    guid = val[dsattributes.kDS1AttrGeneratedUID]
-                    rec = self.recordWithGUID(guid)
-                    if rec:
-                        yield rec
+                    try:
+                        guid = val[dsattributes.kDS1AttrGeneratedUID]
+                        rec = self.recordWithGUID(guid)
+                        if rec:
+                            yield rec
+                    except KeyError:
+                        pass
 
             except Exception, e:
                 self.log_error("OD search failed: %s" % (e,))

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -139,20 +139,21 @@
             for record in self.listRecords(recordType):
                 yield record
 
+    def recordsMatchingFieldsWithCUType(self, fields, operand="or",
+        cuType=None):
+        if cuType:
+            recordType = DirectoryRecord.fromCUType(cuType)
+        else:
+            recordType = None
+
+        return self.recordsMatchingFields(fields, operand=operand,
+            recordType=recordType)
+
+
     def recordsMatchingFields(self, fields, 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, caseless, matchType):
             if caseless:
                 fieldValue = fieldValue.lower()
@@ -194,6 +195,11 @@
                 return False
 
         try:
+            if recordType is None:
+                recordTypes = list(self.recordTypes())
+            else:
+                recordTypes = (recordType,)
+
             for recordType in recordTypes:
                 for record in self.listRecords(recordType):
                     if recordMatches(record):
@@ -288,6 +294,24 @@
     def verifyCredentials(self, credentials):
         return False
 
+    # Mapping from directory record.recordType to RFC2445 CUTYPE values
+    _cuTypes = {
+        'users' : 'INDIVIDUAL',
+        'groups' : 'GROUP',
+        'resources' : 'RESOURCE',
+        'locations' : 'ROOM',
+    }
+
+    def getCUType(self):
+        return self._cuTypes.get(self.recordType, "UNKNOWN")
+
+    @classmethod
+    def fromCUType(cls, cuType):
+        for key, val in cls._cuTypes.iteritems():
+            if val == cuType:
+                return key
+        return None
+
 class DirectoryError(RuntimeError):
     """
     Generic directory error.

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -49,12 +49,13 @@
 
 from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
 from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
-from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.directory.util import NotFilePath
-from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVFile, DAVPrincipalResource
+from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVFile, DAVPrincipalResource, DirectoryPrincipalPropertySearchMixIn
 from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource
 from twistedcaldav.directory.idirectory import IDirectoryService
 from twistedcaldav.log import Logger
+from twistedcaldav import caldavxml, customxml
 
 log = Logger()
 
@@ -75,6 +76,7 @@
 
 
 class DirectoryProvisioningResource (
+    DirectoryPrincipalPropertySearchMixIn,
     PermissionsMixIn,
     CalendarPrincipalCollectionResource,
     DAVFile,
@@ -131,20 +133,51 @@
 
     _cs_ns = "http://calendarserver.org/ns/"
     _fieldMap = {
-        ("DAV:" , "displayname") : "fullName",
-        (_cs_ns, "first-name") : "firstName",
-        (_cs_ns, "last-name") : "lastName",
-        (_cs_ns, "email-address-set") : "emailAddresses",
+        ("DAV:" , "displayname") :
+            ("fullName", None, "Display Name", davxml.DisplayName),
+        ("urn:ietf:params:xml:ns:caldav" , "calendar-user-type") :
+            ("recordType", DirectoryRecord.fromCUType, "Calendar User Type",
+            caldavxml.CalendarUserType),
+        (_cs_ns, "first-name") :
+            ("firstName", None, "First Name", customxml.FirstNameProperty),
+        (_cs_ns, "last-name") :
+            ("lastName", None, "Last Name", customxml.LastNameProperty),
+        (_cs_ns, "email-address-set") :
+            ("emailAddresses", None, "Email Addresses",
+            customxml.EmailAddressProperty),
     }
 
-    def propertyToField(self, property):
+    def propertyToField(self, property, match):
         """
         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)
+        field, converter, description, xmlClass = self._fieldMap.get(
+            property.qname(), (None, None, None))
+        if field is None:
+            return (None, None)
+        elif converter is not None:
+            match = converter(match)
+        return (field, match)
 
+    def principalSearchPropertySet(self):
+        props = []
+        for field, converter, description, xmlClass in self._fieldMap.itervalues():
+            props.append(
+                davxml.PrincipalSearchProperty(
+                    davxml.PropertyContainer(
+                        xmlClass()
+                    ),
+                    davxml.Description(
+                        davxml.PCDATAElement(description),
+                        **{"xml:lang":"en"}
+                    ),
+                )
+            )
 
+        return davxml.PrincipalSearchPropertySet(*props)
+
+
 class DirectoryPrincipalProvisioningResource (DirectoryProvisioningResource):
     """
     Collection resource which provisions directory principals as its children.
@@ -382,7 +415,7 @@
     def principalCollections(self):
         return self.parent.principalCollections()
 
-class DirectoryPrincipalResource (PropfindCacheMixin, PermissionsMixIn, DAVPrincipalResource, DAVFile):
+class DirectoryPrincipalResource (DirectoryPrincipalPropertySearchMixIn, PropfindCacheMixin, PermissionsMixIn, DAVPrincipalResource, DAVFile):
     """
     Directory principal resource.
     """

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -35,7 +35,7 @@
 import time
 
 from twisted.internet.defer import succeed, DeferredList, inlineCallbacks, returnValue
-from twisted.internet.defer import maybeDeferred
+from twisted.internet.defer import maybeDeferred, deferredGenerator, waitForDeferred
 from twisted.web2 import responsecode
 from twisted.web2.http import HTTPError, Response, RedirectResponse
 from twisted.web2.http import StatusResponse
@@ -44,11 +44,15 @@
 from twisted.web2.static import MetaDataMixin
 from twisted.web2.dav import davxml
 from twisted.web2.dav.davxml import dav_namespace
+from twisted.web2.dav.http import ErrorResponse, MultiStatusResponse
 from twisted.web2.dav.static import DAVFile as SuperDAVFile
 from twisted.web2.dav.resource import DAVResource as SuperDAVResource
 from twisted.web2.dav.resource import DAVPrincipalResource as SuperDAVPrincipalResource
 from twisted.web2.dav.util import joinURL
 from twisted.web2.dav.xattrprops import xattrPropertyStore
+from twisted.web2.dav.method import prop_common
+from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
+from twisted.web2.dav.method.report import max_number_of_matches
 
 from twistedcaldav.log import Logger, LoggingMixIn
 from twistedcaldav.util import submodule, Alternator, printTracebacks
@@ -400,6 +404,7 @@
             
         returnValue(match)
 
+
 class DAVPrincipalResource (SuperDAVPrincipalResource, LoggingMixIn):
     """
     Extended L{twisted.web2.dav.static.DAVFile} implementation.
@@ -710,6 +715,8 @@
          )
 
 
+
+
 class ReadOnlyWritePropertiesResourceMixIn (object):
     """
     Read only that will allow writing of properties resource.
@@ -831,3 +838,148 @@
                 for name in super(CachingXattrPropertyStore, self).list()
             )
         return self._data
+
+
+class DirectoryPrincipalPropertySearchMixIn(object):
+
+    def report_DAV__principal_property_search(self, request,
+        principal_property_search):
+        """
+        Generate a principal-property-search REPORT. (RFC 3744, section 9.4)
+        Overrides twisted implementation, targetting only directory-enabled
+        searching.
+        """
+        # Verify root element
+        if not isinstance(principal_property_search,
+            davxml.PrincipalPropertySearch):
+            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"
+
+        # Are we narrowing results down to a single CUTYPE?
+        cuType = principal_property_search.attributes.get("type", None)
+        if cuType not in ("INDIVIDUAL", "GROUP", "RESOURCE", "ROOM", None):
+            raise ValueError("Unknown value for type attribute: %s" % (cuType,))
+
+        # 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
+        propertySearches = []
+        applyTo = False
+        for child in principal_property_search.children:
+            if child.qname() == (dav_namespace, "prop"):
+                propertiesForResource = prop_common.propertyListForResource
+                propElement = child
+            elif child.qname() == (dav_namespace,
+                "apply-to-principal-collection-set"):
+                applyTo = True
+            elif child.qname() == (dav_namespace, "property-search"):
+                props = child.childOfType(davxml.PropertyContainer)
+                props.removeWhitespaceNodes()
+
+                match = child.childOfType(davxml.Match)
+                caseless = match.attributes.get("caseless", "yes")
+                if caseless not in ("yes", "no"):
+                    raise ValueError("Unknown value for caseless attribute: %s"
+                        % (caseless,))
+                caseless = True if caseless == "yes" else False
+                matchType = match.attributes.get("match-type", "contains")
+                if matchType not in ("starts-with", "contains"):
+                    raise ValueError("Unknown value for match-type attribute: %s" %
+                        (matchType,))
+
+                propertySearches.append((props.children, str(match),
+                    caseless, matchType))
+
+        # Run report
+        try:
+
+            resources = []
+            if applyTo or not hasattr(self, "directory"):
+                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))
+
+            # We need to access a directory service
+            principalCollection = resources[0][0]
+            dir = principalCollection.directory
+
+            # See if we can take advantage of the directory
+            fields = []
+            nonDirectorySearches = []
+            for props, match, caseless, matchType in propertySearches:
+                nonDirectoryProps = []
+                for prop in props:
+                    fieldName, match = principalCollection.propertyToField(
+                        prop, match)
+                    if fieldName:
+                        fields.append((fieldName, match, caseless, matchType))
+                    else:
+                        nonDirectoryProps.append(prop)
+                if nonDirectoryProps:
+                    nonDirectorySearches.append((nonDirectoryProps, match,
+                        caseless, matchType))
+
+            matchingResources = []
+            matchcount = 0
+
+            # nonDirectorySearches are ignored
+
+            if fields:
+
+                for record in dir.recordsMatchingFieldsWithCUType(fields,
+                    operand=operand, cuType=cuType):
+
+                    resource = principalCollection.principalForRecord(record)
+
+                    # We've determined this is a matching resource
+                    matchcount += 1
+                    if matchcount > max_number_of_matches:
+                        raise NumberOfMatchesWithinLimits
+                    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(
+                responsecode.FORBIDDEN,
+                (dav_namespace, "number-of-matches-within-limits")
+            ))
+
+        yield MultiStatusResponse(responses)
+
+    report_DAV__principal_property_search = deferredGenerator(report_DAV__principal_property_search)

Modified: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -615,7 +615,7 @@
         organizerProperty = calendar.getOrganizerProperty()
         if organizerProperty is None:
             # ORGANIZER is required per rfc2446 section 3.2.3
-            self.log_warning("Mail gateway didn't find an ORGANIZER in REPLY %s" % (msg['Message-ID'],))
+            self.log_warn("Mail gateway didn't find an ORGANIZER in REPLY %s" % (msg['Message-ID'],))
             calendar.addProperty(Property("ORGANIZER", organizer))
         else:
             organizerProperty.setValue(organizer)

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -693,6 +693,7 @@
         (caldav_namespace, "calendar-user-address-set"),
         (caldav_namespace, "schedule-inbox-URL"       ),
         (caldav_namespace, "schedule-outbox-URL"      ),
+        (caldav_namespace, "calendar-user-type"       ),
         (calendarserver_namespace, "first-name"       ),
         (calendarserver_namespace, "last-name"        ),
         (calendarserver_namespace, "email-address-set"),
@@ -743,6 +744,11 @@
                     else:
                         return caldavxml.ScheduleOutboxURL(davxml.HRef(url))
 
+                if name == "calendar-user-type":
+                    return caldavxml.CalendarUserType(
+                        self.record.getCUType()
+                    )
+
             elif namespace == calendarserver_namespace:
                 if name == "dropbox-home-URL" and config.EnableDropBox:
                     url = self.dropboxURL()

Modified: CalendarServer/trunk/twistedcaldav/root.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/root.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/root.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -28,7 +28,7 @@
 from twisted.web2.http import HTTPError, StatusResponse
 from twisted.web2.auth.wrapper import UnauthorizedResponse
 
-from twistedcaldav.extensions import DAVFile, CachingXattrPropertyStore
+from twistedcaldav.extensions import DAVFile, CachingXattrPropertyStore, DirectoryPrincipalPropertySearchMixIn
 from twistedcaldav.config import config
 from twistedcaldav.cache import _CachedResponseResource
 from twistedcaldav.cache import MemcacheResponseCache, MemcacheChangeNotifier
@@ -49,7 +49,7 @@
         return succeed(self.defaultAccessControlList())
 
 
-class RootResource (RootACLMixIn, DAVFile):
+class RootResource (DirectoryPrincipalPropertySearchMixIn, RootACLMixIn, DAVFile):
     """
     A special root resource that contains support checking SACLs
     as well as adding responseFilters.

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -892,7 +892,7 @@
         
         for original, filtered, attendees in data:
             component = Component.fromString(original)
-            itipped = iTipGenerator.generateAttendeeRequest(component, attendees)
+            itipped = iTipGenerator.generateAttendeeRequest(component, attendees, None)
             itipped = str(itipped).replace("\r", "")
             itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
             self.assertEqual(filtered, itipped)

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2008-10-09 21:33:55 UTC (rev 3137)
+++ CalendarServer/trunk/twistedcaldav/static.py	2008-10-10 17:34:50 UTC (rev 3138)
@@ -57,7 +57,7 @@
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
-from twistedcaldav.extensions import DAVFile
+from twistedcaldav.extensions import DAVFile, DirectoryPrincipalPropertySearchMixIn
 from twistedcaldav.extensions import CachingXattrPropertyStore
 from twistedcaldav.freebusyurl import FreeBusyURLResource
 from twistedcaldav.ical import Component as iComponent
@@ -490,7 +490,7 @@
 
         return True
 
-class CalendarHomeProvisioningFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeProvisioningResource, DAVFile):
+class CalendarHomeProvisioningFile (DirectoryPrincipalPropertySearchMixIn, AutoProvisioningFileMixIn, DirectoryCalendarHomeProvisioningResource, DAVFile):
     """
     Resource which provisions calendar home collections as needed.
     """
@@ -512,7 +512,7 @@
     def createSimilarFile(self, path):
         raise HTTPError(responsecode.NOT_FOUND)
 
-class CalendarHomeTypeProvisioningFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeTypeProvisioningResource, DAVFile):
+class CalendarHomeTypeProvisioningFile (DirectoryPrincipalPropertySearchMixIn, AutoProvisioningFileMixIn, DirectoryCalendarHomeTypeProvisioningResource, DAVFile):
     def __init__(self, path, parent, recordType):
         """
         @param path: the path to the file which will back the resource.
@@ -601,7 +601,7 @@
     def createSimilarFile(self, path):
         raise HTTPError(responsecode.NOT_FOUND)
 
-class CalendarHomeFile (PropfindCacheMixin, AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
+class CalendarHomeFile (DirectoryPrincipalPropertySearchMixIn, PropfindCacheMixin, AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
     """
     Calendar home collection resource.
     """
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081010/e391bf54/attachment-0001.html 


More information about the calendarserver-changes mailing list