<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[12369] twext/trunk/twext/who/ldap</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/12369">12369</a></dd>
<dt>Author</dt> <dd>wsanchez@apple.com</dd>
<dt>Date</dt> <dd>2014-01-16 19:02:21 -0800 (Thu, 16 Jan 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Change the field name map so that it maps to multiple LDAP attributes.

Change the record type map so that it maps to multiple LDAP object classes.

Much fallout ensues.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#twexttrunktwextwholdap_servicepy">twext/trunk/twext/who/ldap/_service.py</a></li>
<li><a href="#twexttrunktwextwholdap_utilpy">twext/trunk/twext/who/ldap/_util.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_servicepy">twext/trunk/twext/who/ldap/test/test_service.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_utilpy">twext/trunk/twext/who/ldap/test/test_util.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="twexttrunktwextwholdap_servicepy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/_service.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_service.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/_service.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -49,21 +49,21 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-# Maps field name -&gt; LDAP attribute name
-DEFAULT_FIELDNAME_MAP = {
-    BaseFieldName.guid: LDAPAttribute.generatedUUID.value,
-    BaseFieldName.recordType: LDAPAttribute.objectClass.value,
-    BaseFieldName.shortNames: LDAPAttribute.uid.value,
-    BaseFieldName.fullNames: LDAPAttribute.cn.value,
-    BaseFieldName.emailAddresses: LDAPAttribute.mail.value,
-    BaseFieldName.password: LDAPAttribute.userPassword.value,
</del><ins>+# Maps field name -&gt; LDAP attribute names
+DEFAULT_FIELDNAME_ATTRIBUTE_MAP = {
+    BaseFieldName.guid: (LDAPAttribute.generatedUUID.value,),
+    BaseFieldName.recordType: (LDAPAttribute.objectClass.value,),
+    BaseFieldName.shortNames: (LDAPAttribute.uid.value,),
+    BaseFieldName.fullNames: (LDAPAttribute.cn.value,),
+    BaseFieldName.emailAddresses: (LDAPAttribute.mail.value,),
+    BaseFieldName.password: (LDAPAttribute.userPassword.value,),
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-# Maps record type -&gt; LDAP object class name
-DEFAULT_RECORDTYPE_MAP = {
-    BaseRecordType.user: LDAPObjectClass.person.value,
-    BaseRecordType.group: LDAPObjectClass.groupOfNames.value,
</del><ins>+# Maps record type -&gt; LDAP object class names
+DEFAULT_RECORDTYPE_OBJECTCLASS_MAP = {
+    BaseRecordType.user: (LDAPObjectClass.person.value,),
+    BaseRecordType.group: (LDAPObjectClass.groupOfNames.value,),
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -147,8 +147,8 @@
</span><span class="cx">         tlsCACertificateFile=None,
</span><span class="cx">         tlsCACertificateDirectory=None,
</span><span class="cx">         useTLS=False,
</span><del>-        fieldNameToAttributeMap=DEFAULT_FIELDNAME_MAP,
-        recordTypeToObjectClassMap=DEFAULT_RECORDTYPE_MAP,
</del><ins>+        fieldNameToAttributeMap=DEFAULT_FIELDNAME_ATTRIBUTE_MAP,
+        recordTypeToObjectClassMap=DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
</ins><span class="cx">         uidField=BaseFieldName.uid,
</span><span class="cx">         _debug=False,
</span><span class="cx">     ):
</span><span class="lines">@@ -178,12 +178,12 @@
</span><span class="cx">         @param fieldNameToAttributeMap: A mapping of field names to LDAP
</span><span class="cx">             attribute names.
</span><span class="cx">         @type fieldNameToAttributeMap: mapping with L{NamedConstant} keys and
</span><del>-            L{unicode} values
</del><ins>+            sequence of L{unicode} values
</ins><span class="cx"> 
</span><span class="cx">         @param recordTypeToObjectClassMap: A mapping of record types to LDAP
</span><span class="cx">             object classes.
</span><span class="cx">         @type recordTypeToObjectClassMap: mapping with L{NamedConstant} keys
</span><del>-            and L{unicode} values
</del><ins>+            and sequence of L{unicode} values
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         self.url = url
</span><span class="lines">@@ -208,24 +208,28 @@
</span><span class="cx">         else:
</span><span class="cx">             self._debug = None
</span><span class="cx"> 
</span><del>-        def reverseDict(source):
</del><ins>+        def reverseDict(sourceName, source):
</ins><span class="cx">             new = {}
</span><span class="cx"> 
</span><del>-            for k, v in source.iteritems():
-                if v in new:
-                    raise LDAPConfigurationError(
-                        u&quot;Field name map has duplicate values: {0}&quot;.format(v)
-                    )
-                new[v] = k
</del><ins>+            for key, values in source.iteritems():
+                for value in values:
+                    if value in new:
+                        raise LDAPConfigurationError(
+                            u&quot;{0} map has duplicate values: {1}&quot;
+                            .format(sourceName, value)
+                        )
+                    new[value] = key
</ins><span class="cx"> 
</span><span class="cx">             return new
</span><span class="cx"> 
</span><span class="cx">         self._fieldNameToAttributeMap = fieldNameToAttributeMap
</span><del>-        self._attributeToFieldNameMap = reverseDict(fieldNameToAttributeMap)
</del><ins>+        self._attributeToFieldNameMap = reverseDict(
+            &quot;Field name&quot;, fieldNameToAttributeMap
+        )
</ins><span class="cx"> 
</span><span class="cx">         self._recordTypeToObjectClassMap = recordTypeToObjectClassMap
</span><span class="cx">         self._objectClassToRecordTypeMap = reverseDict(
</span><del>-            recordTypeToObjectClassMap
</del><ins>+            &quot;Record type&quot;, recordTypeToObjectClassMap
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         self._uidField = uidField
</span><span class="lines">@@ -317,23 +321,28 @@
</span><span class="cx">         # we are using to determine the UID of the record.
</span><span class="cx"> 
</span><span class="cx">         uidField = self.fieldName.uid
</span><del>-        uidAttribute = self._fieldNameToAttributeMap[self._uidField]
</del><ins>+        uidAttribute = self._fieldNameToAttributeMap[self._uidField][0]
</ins><span class="cx"> 
</span><span class="cx">         recordTypeField = self.fieldName.recordType
</span><del>-        recordTypeAttribute = (
</del><ins>+        recordTypeAttributes = (
</ins><span class="cx">             self._fieldNameToAttributeMap[self.fieldName.recordType]
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         for dn, recordData in reply:
</span><span class="cx"> 
</span><del>-            if recordTypeAttribute not in recordData:
-                self.log.debug(
-                    &quot;Ignoring LDAP record data with no record type attribute &quot;
-                    &quot;{source.fieldName.recordType!r}: {recordData!r}&quot;,
-                    self=self, recordData=recordData
-                )
-                continue
</del><ins>+            # Attributes used to determine the record type are required, since
+            # record type is very much required.
</ins><span class="cx"> 
</span><ins>+            for recordTypeAttribute in recordTypeAttributes:
+                if recordTypeAttribute not in recordData:
+                    self.log.debug(
+                        &quot;Ignoring LDAP record data without record type &quot;
+                        &quot;attribute {attribute!r}: &quot;
+                        &quot;{recordData!r}&quot;,
+                        attribute=recordTypeAttribute, recordData=recordData,
+                    )
+                    continue
+
</ins><span class="cx">             # Make a dict of fields -&gt; values from the incoming dict of
</span><span class="cx">             # attributes -&gt; values.
</span><span class="cx"> 
</span><span class="lines">@@ -351,7 +360,7 @@
</span><span class="cx">                 self.log.debug(
</span><span class="cx">                     &quot;Ignoring LDAP record data with no UID attribute &quot;
</span><span class="cx">                     &quot;{source._uidField!r}: {recordData!r}&quot;,
</span><del>-                    self=self, recordData=recordData
</del><ins>+                    recordData=recordData
</ins><span class="cx">                 )
</span><span class="cx">                 continue
</span><span class="cx"> 
</span></span></pre></div>
<a id="twexttrunktwextwholdap_utilpy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/_util.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_util.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/_util.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -21,12 +21,37 @@
</span><span class="cx">     MatchExpression, MatchFlags,
</span><span class="cx"> )
</span><span class="cx"> from ..util import iterFlags
</span><del>-from ._constants import LDAPMatchType
</del><ins>+from ._constants import LDAPOperand, LDAPMatchType, LDAPMatchFlags
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+def ldapQueryStringFromQueryStrings(operand, queryStrings):
+    &quot;&quot;&quot;
+    Combines LDAP query strings into a single query string.
+
+    @param operand: An LDAP operand (C{u&quot;&amp;&quot;} or C{u&quot;|&quot;}).
+    @type operand: L{unicode}
+
+    @param queryStrings: LDAP query strings.
+    @type queryStrings: iterable of L{unicode}
+    &quot;&quot;&quot;
+    if len(queryStrings) == 1:
+        return queryStrings[0]
+
+    elif len(queryStrings) &gt; 1:
+        queryTokens = []
+        queryTokens.append(u&quot;(&quot;)
+        queryTokens.append(operand)
+        queryTokens.extend(queryStrings)
+        queryTokens.append(u&quot;)&quot;)
+        return u&quot;&quot;.join(queryTokens)
+
+    else:
+        return u&quot;&quot;
+
+
</ins><span class="cx"> def ldapQueryStringFromMatchExpression(
</span><del>-    expression, fieldNameMap, recordTypeMap
</del><ins>+    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Generates an LDAP query string from a match expression.
</span><span class="lines">@@ -34,21 +59,24 @@
</span><span class="cx">     @param expression: A match expression.
</span><span class="cx">     @type expression: L{MatchExpression}
</span><span class="cx"> 
</span><del>-    @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
-        names.
-    @type fieldNameMap: L{dict}
</del><ins>+    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+        attribute names.
+    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+        of L{unicode} values.
</ins><span class="cx"> 
</span><del>-    @param recordTypeMap: A mapping from L{RecordType}s to native LDAP object
-        class names.
-    @type recordTypeMap: L{dict}
</del><ins>+    @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+        LDAP object class names.
+    @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+        sequence of L{unicode} values.
</ins><span class="cx"> 
</span><span class="cx">     @return: An LDAP query string.
</span><del>-    @rtype: C{unicode}
</del><ins>+    @rtype: L{unicode}
</ins><span class="cx"> 
</span><span class="cx">     @raises QueryNotSupportedError: If the expression's match type is unknown,
</span><span class="cx">         or if the expresion references an unknown field name (meaning a field
</span><del>-        name not in C{fieldNameMap}).
</del><ins>+        name not in C{fieldNameToAttributeMap}).
</ins><span class="cx">     &quot;&quot;&quot;
</span><ins>+
</ins><span class="cx">     matchType = LDAPMatchType.fromMatchType(expression.matchType)
</span><span class="cx">     if matchType is None:
</span><span class="cx">         raise QueryNotSupportedError(
</span><span class="lines">@@ -58,7 +86,7 @@
</span><span class="cx">     flags = tuple(iterFlags(expression.flags))
</span><span class="cx"> 
</span><span class="cx">     if MatchFlags.NOT in flags:
</span><del>-        notOp = u&quot;!&quot;
</del><ins>+        notOp = LDAPMatchFlags.NOT.value
</ins><span class="cx">     else:
</span><span class="cx">         notOp = u&quot;&quot;
</span><span class="cx"> 
</span><span class="lines">@@ -71,7 +99,7 @@
</span><span class="cx"> 
</span><span class="cx">     fieldName = expression.fieldName
</span><span class="cx">     try:
</span><del>-        attribute = fieldNameMap[fieldName]
</del><ins>+        attributes = fieldNameToAttributeMap[fieldName]
</ins><span class="cx">     except KeyError:
</span><span class="cx">         raise QueryNotSupportedError(
</span><span class="cx">             &quot;Unmapped field name: {0}&quot;.format(expression.fieldName)
</span><span class="lines">@@ -79,23 +107,49 @@
</span><span class="cx"> 
</span><span class="cx">     if fieldName is FieldName.recordType:
</span><span class="cx">         try:
</span><del>-            value = recordTypeMap[expression.fieldValue]
</del><ins>+            values = recordTypeToObjectClassMap[expression.fieldValue]
</ins><span class="cx">         except KeyError:
</span><span class="cx">             raise QueryNotSupportedError(
</span><span class="cx">                 &quot;Unmapped record type: {0}&quot;.format(expression.fieldValue)
</span><span class="cx">             )
</span><span class="cx">     else:
</span><del>-        value = unicode(expression.fieldValue)
</del><ins>+        values = (unicode(expression.fieldValue),)
</ins><span class="cx"> 
</span><del>-    value = value.translate(LDAP_QUOTING_TABLE)  # Escape special chars
</del><ins>+    # Escape special LDAP query characters
+    values = [value.translate(LDAP_QUOTING_TABLE) for value in values]
+    del value  # Symbol used below; ensure non-reuse of data
</ins><span class="cx"> 
</span><del>-    return matchType.queryString.format(
-        notOp=notOp, attribute=attribute, value=value
-    )
</del><ins>+    # Compose an query using each of the LDAP attributes cooresponding to the
+    # target field name.
</ins><span class="cx"> 
</span><ins>+    if notOp:
+        operand = LDAPOperand.AND.value
+    else:
+        operand = LDAPOperand.OR.value
</ins><span class="cx"> 
</span><ins>+    if notOp:
+        valueOperand = LDAPOperand.OR.value
+    else:
+        valueOperand = LDAPOperand.AND.value
+
+    queryStrings = [
+        ldapQueryStringFromQueryStrings(
+            valueOperand,
+            [
+                matchType.queryString.format(
+                    notOp=notOp, attribute=attribute, value=value
+                )
+                for value in values
+            ]
+        )
+        for attribute in attributes
+    ]
+
+    return ldapQueryStringFromQueryStrings(operand, queryStrings)
+
+
</ins><span class="cx"> def ldapQueryStringFromCompoundExpression(
</span><del>-    expression, fieldNameMap, recordTypeMap
</del><ins>+    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Generates an LDAP query string from a compound expression.
</span><span class="lines">@@ -103,67 +157,73 @@
</span><span class="cx">     @param expression: A compound expression.
</span><span class="cx">     @type expression: L{MatchExpression}
</span><span class="cx"> 
</span><del>-    @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
-        names.
-    @type fieldNameMap: L{dict}
</del><ins>+    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+        attribute names.
+    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+        of L{unicode} values.
</ins><span class="cx"> 
</span><ins>+    @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+        LDAP object class names.
+    @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+        sequence of L{unicode} values.
+
</ins><span class="cx">     @return: An LDAP query string.
</span><del>-    @rtype: C{unicode}
</del><ins>+    @rtype: L{unicode}
</ins><span class="cx"> 
</span><span class="cx">     @raises QueryNotSupportedError: If any sub-expression cannot be converted
</span><span class="cx">         to an LDAP query.
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    queryTokens = []
</del><ins>+    if expression.operand is Operand.AND:
+        operand = LDAPOperand.AND.value
</ins><span class="cx"> 
</span><del>-    if len(expression.expressions) &gt; 1:
-        queryTokens.append(u&quot;(&quot;)
</del><ins>+    elif expression.operand is Operand.OR:
+        operand = LDAPOperand.OR.value
</ins><span class="cx"> 
</span><del>-        if expression.operand is Operand.AND:
-            queryTokens.append(u&quot;&amp;&quot;)
-        else:
-            queryTokens.append(u&quot;|&quot;)
-
-    for subExpression in expression.expressions:
-        queryTokens.append(
-            ldapQueryStringFromExpression(
-                subExpression, fieldNameMap, recordTypeMap
-            )
</del><ins>+    queryStrings = [
+        ldapQueryStringFromExpression(
+            subExpression,
+            fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx">         )
</span><ins>+        for subExpression in expression.expressions
+    ]
</ins><span class="cx"> 
</span><del>-    if len(expression.expressions) &gt; 1:
-        queryTokens.append(u&quot;)&quot;)
</del><ins>+    return ldapQueryStringFromQueryStrings(operand, queryStrings)
</ins><span class="cx"> 
</span><del>-    return u&quot;&quot;.join(queryTokens)
</del><span class="cx"> 
</span><del>-
</del><span class="cx"> def ldapQueryStringFromExpression(
</span><del>-    expression, fieldNameMap, recordTypeMap
</del><ins>+    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Converts an expression into an LDAP query string.
</span><span class="cx"> 
</span><del>-    @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
-        names.
-    @type fieldNameMap: L{dict}
-
</del><span class="cx">     @param expression: An expression.
</span><span class="cx">     @type expression: L{MatchExpression} or L{CompoundExpression}
</span><span class="cx"> 
</span><del>-    @return: A native OpenDirectory query string
-    @rtype: C{unicode}
</del><ins>+    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+        attribute names.
+    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+        of L{unicode} values.
</ins><span class="cx"> 
</span><ins>+    @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+        LDAP object class names.
+    @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+        sequence of L{unicode} values.
+
+    @return: An LDAP query string.
+    @rtype: L{unicode}
+
</ins><span class="cx">     @raises QueryNotSupportedError: If the expression cannot be converted to an
</span><span class="cx">         LDAP query.
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">     if isinstance(expression, MatchExpression):
</span><span class="cx">         return ldapQueryStringFromMatchExpression(
</span><del>-            expression, fieldNameMap, recordTypeMap
</del><ins>+            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">     if isinstance(expression, CompoundExpression):
</span><span class="cx">         return ldapQueryStringFromCompoundExpression(
</span><del>-            expression, fieldNameMap, recordTypeMap
</del><ins>+            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">     raise QueryNotSupportedError(
</span></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_servicepy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/test/test_service.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_service.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/test/test_service.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -20,6 +20,8 @@
</span><span class="cx"> LDAP directory service tests.
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><ins>+from itertools import chain
+
</ins><span class="cx"> import ldap
</span><span class="cx"> from mockldap import MockLdap
</span><span class="cx"> 
</span><span class="lines">@@ -31,7 +33,7 @@
</span><span class="cx"> 
</span><span class="cx"> from ...idirectory import QueryNotSupportedError, FieldName as BaseFieldName
</span><span class="cx"> from .._service import (
</span><del>-    DEFAULT_FIELDNAME_MAP, DEFAULT_RECORDTYPE_MAP,
</del><ins>+    DEFAULT_FIELDNAME_ATTRIBUTE_MAP, DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
</ins><span class="cx">     LDAPBindAuthError,
</span><span class="cx">     DirectoryService, DirectoryRecord,
</span><span class="cx"> )
</span><span class="lines">@@ -45,8 +47,8 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-TEST_FIELDNAME_MAP = dict(DEFAULT_FIELDNAME_MAP)
-TEST_FIELDNAME_MAP[BaseFieldName.uid] = u&quot;__who_uid__&quot;
</del><ins>+TEST_FIELDNAME_MAP = dict(DEFAULT_FIELDNAME_ATTRIBUTE_MAP)
+TEST_FIELDNAME_MAP[BaseFieldName.uid] = (u&quot;__who_uid__&quot;,)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -236,14 +238,16 @@
</span><span class="cx">         return unicode(obj)
</span><span class="cx"> 
</span><span class="cx">     def tuplify(record, fieldName):
</span><del>-        name = TEST_FIELDNAME_MAP.get(fieldName, fieldName.name)
-
</del><span class="cx">         if fieldName is BaseFieldName.recordType:
</span><del>-            value = DEFAULT_RECORDTYPE_MAP[record.fields[fieldName]]
</del><ins>+            values = DEFAULT_RECORDTYPE_OBJECTCLASS_MAP[
+                record.fields[fieldName]
+            ]
</ins><span class="cx">         else:
</span><del>-            value = toUnicode(record.fields[fieldName])
</del><ins>+            values = (toUnicode(record.fields[fieldName]),)
</ins><span class="cx"> 
</span><del>-        return (name, value)
</del><ins>+        for name in TEST_FIELDNAME_MAP.get(fieldName, fieldName.name):
+            for value in values:
+                yield (name, value)
</ins><span class="cx"> 
</span><span class="cx">     for records in service.index[service.fieldName.uid].itervalues():
</span><span class="cx">         for record in records:
</span><span class="lines">@@ -253,11 +257,11 @@
</span><span class="cx">                 dc1=dc1, dc0=dc0
</span><span class="cx">             )
</span><span class="cx"> 
</span><del>-            recordData = dict(
-                tuplify(record, fieldName)
</del><ins>+            recordData = dict(chain(*(
+                list(tuplify(record, fieldName))
</ins><span class="cx">                 for fieldName in service.fieldName.iterconstants()
</span><span class="cx">                 if fieldName in record.fields
</span><del>-            )
</del><ins>+            )))
</ins><span class="cx"> 
</span><span class="cx">             data[dn] = recordData
</span><span class="cx"> 
</span></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_utilpy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/test/test_util.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_util.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/test/test_util.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -24,8 +24,10 @@
</span><span class="cx"> from ...expression import (
</span><span class="cx">     CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
</span><span class="cx"> )
</span><ins>+from .._constants import LDAPOperand
</ins><span class="cx"> from .._service import DirectoryService
</span><span class="cx"> from .._util import (
</span><ins>+    ldapQueryStringFromQueryStrings,
</ins><span class="cx">     ldapQueryStringFromMatchExpression,
</span><span class="cx">     ldapQueryStringFromCompoundExpression,
</span><span class="cx">     ldapQueryStringFromExpression,
</span><span class="lines">@@ -51,7 +53,10 @@
</span><span class="cx">         but we don't care for these tests, since we're not actually connecting
</span><span class="cx">         to LDAP.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return dict([(c, c.name) for c in service.fieldName.iterconstants()])
</del><ins>+        return dict([
+            (c, (c.name,))
+            for c in service.fieldName.iterconstants()
+        ])
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def recordTypeMap(self, service):
</span><span class="lines">@@ -61,9 +66,44 @@
</span><span class="cx">         names, but we don't care for these tests, since we're not actually
</span><span class="cx">         connecting to LDAP.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        return dict([(c, c.name) for c in service.recordType.iterconstants()])
</del><ins>+        return dict([
+            (c, (c.name,))
+            for c in service.recordType.iterconstants()
+        ])
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def _test_ldapQueryStringFromQueryStrings(self, queryStrings, expected):
+        for operand in (LDAPOperand.AND.value, LDAPOperand.OR.value):
+            compound = ldapQueryStringFromQueryStrings(operand, queryStrings)
+            self.assertEquals(compound, expected.format(operand=operand))
+
+
+    def test_ldapQueryStringFromQueryStrings_empty(self):
+        &quot;&quot;&quot;
+        A single expression should just be returned as-is.
+        &quot;&quot;&quot;
+        return self._test_ldapQueryStringFromQueryStrings((), u&quot;&quot;)
+
+
+    def test_ldapQueryStringFromQueryStrings_single(self):
+        &quot;&quot;&quot;
+        A single expression should just be returned as-is.
+        &quot;&quot;&quot;
+        queryStrings = (u&quot;(x=yzzy)&quot;,)
+        return self._test_ldapQueryStringFromQueryStrings(
+            queryStrings, queryStrings[0]
+        )
+
+
+    def test_ldapQueryStringFromQueryStrings_multiple(self):
+        &quot;&quot;&quot;
+        Multiple expressions should just be combined with an operator.
+        &quot;&quot;&quot;
+        return self._test_ldapQueryStringFromQueryStrings(
+            (u&quot;(x=yzzy)&quot;, u&quot;(xy=zzy)&quot;), u&quot;({operand}(x=yzzy)(xy=zzy))&quot;
+        )
+
+
</ins><span class="cx">     def test_queryStringFromMatchExpression_matchTypes(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Match expressions with each match type produces the correct
</span><span class="lines">@@ -202,6 +242,110 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def _test_queryStringFromMatchExpression_multiAttribute(
+        self, flags, expected
+    ):
+        service = self.service()
+
+        expression = MatchExpression(
+            service.fieldName.emailAddresses, u&quot;xyzzy&quot;,
+            flags=flags,
+        )
+
+        fieldNameToAttributeMap = {
+            service.fieldName.emailAddresses: (u&quot;mail&quot;, u&quot;alternateMail&quot;),
+        }
+
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, fieldNameToAttributeMap, self.recordTypeMap(service)
+        )
+
+        self.assertEquals(queryString, expected)
+
+
+    def test_queryStringFromMatchExpression_multipleAttribute(self):
+        &quot;&quot;&quot;
+        Match expression when the queried field name maps to multiple
+        attributes.
+        &quot;&quot;&quot;
+
+        # We want a match for either attribute.
+        expected = u&quot;(|(mail=xyzzy)(alternateMail=xyzzy))&quot;
+
+        return self._test_queryStringFromMatchExpression_multiAttribute(
+            MatchFlags.none, expected
+        )
+
+
+    def test_queryStringFromMatchExpression_multipleAttribute_not(self):
+        &quot;&quot;&quot;
+        Match expression when the queried field name maps to multiple
+        attributes and the NOT flag is set.
+        &quot;&quot;&quot;
+
+        # We want a NOT match for both attributes.
+        expected = u&quot;(&amp;(!mail=xyzzy)(!alternateMail=xyzzy))&quot;
+
+        return self._test_queryStringFromMatchExpression_multiAttribute(
+            MatchFlags.NOT, expected
+        )
+
+
+    def _test_queryStringFromMatchExpression_multiRecordType(
+        self, flags, expected
+    ):
+        service = self.service()
+
+        recordTypeField = service.fieldName.recordType
+
+        expression = MatchExpression(
+            recordTypeField, service.recordType.user,
+            flags=flags,
+        )
+
+        fieldNameToAttributeMap = self.fieldNameMap(service)
+
+        recordTypeToObjectClassMap = {
+            service.recordType.user: (u&quot;person&quot;, u&quot;account&quot;),
+        }
+
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
+        )
+
+        self.assertEquals(
+            queryString,
+            expected.format(attr=fieldNameToAttributeMap[recordTypeField][0])
+        )
+
+
+    def test_queryStringFromMatchExpression_multipleRecordType(self):
+        &quot;&quot;&quot;
+        Match expression when the queried field name is the record type field,
+        which maps to multiple attributes.
+        &quot;&quot;&quot;
+
+        # We want a match for both values.
+        expected = u&quot;(&amp;({attr}=person)({attr}=account))&quot;
+
+        return self._test_queryStringFromMatchExpression_multiRecordType(
+            MatchFlags.none, expected
+        )
+
+    def test_queryStringFromMatchExpression_multipleRecordType_not(self):
+        &quot;&quot;&quot;
+        Match expression when the queried field name is the record type field,
+        which maps to multiple attributes and the NOT flag is set.
+        &quot;&quot;&quot;
+
+        # We want a NOT match for either value.
+        expected = u&quot;(|(!{attr}=person)(!{attr}=account))&quot;
+
+        return self._test_queryStringFromMatchExpression_multiRecordType(
+            MatchFlags.NOT, expected
+        )
+
+
</ins><span class="cx">     def test_queryStringFromCompoundExpression_single(
</span><span class="cx">         self, queryFunction=ldapQueryStringFromCompoundExpression
</span><span class="cx">     ):
</span></span></pre>
</div>
</div>

</body>
</html>