Country validation
authorEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 24 May 2022 02:37:55 +0000 (14:37 +1200)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Fri, 27 May 2022 08:09:15 +0000 (20:09 +1200)
m

CRM/Contact/BAO/Contact.php
CRM/Contact/Import/Parser/Contact.php
CRM/Import/Parser.php
tests/phpunit/CRM/Contact/BAO/ContactTest.php
tests/phpunit/CRM/Contact/Import/Form/data/individual_country_state_county_with_related.csv [new file with mode: 0644]
tests/phpunit/CRM/Contact/Import/Parser/ContactTest.php

index 303d8962ab4356397292fc18410dd120b56dbcaa..9165d43b151fbd5309fba92f26df7dc3a14bcc55 100644 (file)
@@ -807,19 +807,6 @@ WHERE     civicrm_contact.id = " . CRM_Utils_Type::escape($id, 'Integer');
             $values['vcard_name'] = $vcardNames[$values['location_type_id']];
           }
 
-          if (!CRM_Utils_Array::lookupValue($values,
-              'country',
-              CRM_Core_PseudoConstant::country(),
-              $reverse
-            ) &&
-            $reverse
-          ) {
-            CRM_Utils_Array::lookupValue($values,
-              'country',
-              CRM_Core_PseudoConstant::countryIsoCode(),
-              $reverse
-            );
-          }
           $stateProvinceID = self::resolveStateProvinceID($values, $values['country_id'] ?? NULL);
           if ($stateProvinceID) {
             $values['state_province_id'] = $stateProvinceID;
index 72e5d405c6bb0c275e83d0bbf8cc165e3f143c58..a79a94474430652213a7d153443c4ecdbfe448a1 100644 (file)
@@ -932,7 +932,8 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
         //For address custom fields, we do get actual custom field value as an inner array of
         //values so need to modify
         if (array_key_exists($customFieldID, $addressCustomFields)) {
-          $value = $value[0][$key];
+          $locationTypeID = array_key_first($value);
+          $value = $value[$locationTypeID][$key];
           $errors[] = $parser->validateCustomField($customFieldID, $value, $addressCustomFields[$customFieldID], $dateType);
         }
         else {
@@ -981,9 +982,6 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
     }
 
     foreach ($params as $key => $value) {
-      if ($value === 'invalid_import_value') {
-        $errors[] = $this->getFieldMetadata($key)['title'];
-      }
       if ($value) {
 
         switch ($key) {
@@ -1014,33 +1012,6 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
             }
             break;
 
-          case 'country':
-            if (!empty($value)) {
-              foreach ($value as $stateValue) {
-                if ($stateValue['country']) {
-                  CRM_Core_PseudoConstant::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
-                  CRM_Core_PseudoConstant::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
-                  $limitCodes = CRM_Core_BAO_Country::countryLimit();
-                  //If no country is selected in
-                  //localization then take all countries
-                  if (empty($limitCodes)) {
-                    $limitCodes = $countryIsoCodes;
-                  }
-
-                  if (self::in_value($stateValue['country'], $limitCodes) || self::in_value($stateValue['country'], CRM_Core_PseudoConstant::country())) {
-                    continue;
-                  }
-                  if (self::in_value($stateValue['country'], $countryIsoCodes) || self::in_value($stateValue['country'], $countryNames)) {
-                    $errors[] = ts('Country input value is in table but not "available": "This Country is valid but is NOT in the list of Available Countries currently configured for your site. This can be viewed and modifed from Administer > Localization > Languages Currency Locations." ');
-                  }
-                  else {
-                    $errors[] = ts('Country input value not in country table: "The Country value appears to be invalid. It does not match any value in CiviCRM table of countries."');
-                  }
-                }
-              }
-            }
-            break;
-
           case 'county':
             if (!empty($value)) {
               foreach ($value as $county) {
@@ -1531,18 +1502,8 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
               $data['address'][$loc]['state_province'] = $value;
             }
           }
-          elseif ($fieldName === 'country') {
-            // CRM-3393
-            if (is_numeric($value) && ((int ) $value) >= 1000
-            ) {
-              $data['address'][$loc]['country_id'] = $value;
-            }
-            elseif (empty($value)) {
-              $data['address'][$loc]['country_id'] = '';
-            }
-            else {
-              $data['address'][$loc]['country'] = $value;
-            }
+          elseif ($fieldName === 'country_id') {
+            $data['address'][$loc]['country_id'] = $value;
           }
           elseif ($fieldName === 'county') {
             $data['address'][$loc]['county_id'] = $value;
@@ -2547,7 +2508,7 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
 
     $addressFields = [
       'county',
-      'country',
+      'country_id',
       'state_province',
       'supplemental_address_1',
       'supplemental_address_2',
@@ -2753,11 +2714,14 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
   public function validateValues(array $values): void {
     $params = $this->getMappedRow($values);
     $contacts = array_merge(['0' => $params], $this->getRelatedContactsParams($params));
+    $errors = [];
     foreach ($contacts as $value) {
       // If we are referencing a related contact, or are in update mode then we
       // don't need all the required fields if we have enough to find an existing contact.
       $useExistingMatchFields = !empty($value['relationship_type_id']) || $this->isUpdateExistingContacts();
-      $this->validateRequiredContactFields($value['contact_type'], $value, $useExistingMatchFields, !empty($value['relationship_label']) ? '(' . $value['relationship_label'] . ')' : '');
+      $prefixString = !empty($value['relationship_label']) ? '(' . $value['relationship_label'] . ') ' : '';
+      $this->validateRequiredContactFields($value['contact_type'], $value, $useExistingMatchFields, $prefixString);
+      $errors = array_merge($errors, $this->getInvalidValuesForContact($value, $prefixString));
     }
 
     //check for duplicate external Identifier
@@ -2775,7 +2739,7 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
 
     //date-format part ends
 
-    $errorMessage = NULL;
+    $errorMessage = implode(', ', $errors);
     //checking error in custom data
     $this->isErrorInCustomData($params, $errorMessage, $params['contact_sub_type'] ?? NULL);
 
@@ -2787,6 +2751,29 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
     }
   }
 
+  /**
+   * Get the invalid values in the params for the given contact.
+   *
+   * @param array|int|string $value
+   * @param string $prefixString
+   *
+   * @return array
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\NotImplementedException
+   */
+  protected function getInvalidValuesForContact($value, string $prefixString): array {
+    $errors = [];
+    foreach ($value as $contactKey => $contactValue) {
+      if (!preg_match('/^\d+_[a|b]_[a|b]$/', $contactKey)) {
+        $result = $this->getInvalidValues($contactValue, $contactKey, $prefixString);
+        if (!empty($result)) {
+          $errors = array_merge($errors, $result);
+        }
+      }
+    }
+    return $errors;
+  }
+
   /**
    * Get the field mappings for the import.
    *
@@ -2879,9 +2866,39 @@ class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
    */
   private function addFieldToParams(array &$contactArray, array $locationValues, string $fieldName, $importedValue): void {
     if (!empty($locationValues)) {
-      $locationValues[$fieldName] = $importedValue;
-      $contactArray[$fieldName] = (array) ($contactArray[$fieldName] ?? []);
-      $contactArray[$fieldName][] = $locationValues;
+      $fieldMap = ['country' => 'country_id'];
+      $realFieldName = empty($fieldMap[$fieldName]) ? $fieldName : $fieldMap[$fieldName];
+      $entity = strtolower($this->getFieldEntity($fieldName));
+      // The entity key is either location_type_id for address, email - eg. 1, or
+      // location_type_id + '_' + phone_type_id or im_provider_id
+      // or the value for website(since websites are not historically one-per-type)
+      $entityKey = $locationValues['location_type_id'] ?? $importedValue;
+      if (!empty($locationValues['phone_type_id']) || !empty($locationValues['provider_id'])) {
+        $entityKey .= '_' . ($locationValues['phone_type_id'] ?? '' . $locationValues['provider_id'] ?? '');
+      }
+      $fieldValue = $this->getTransformedFieldValue($realFieldName, $importedValue);
+      $availableCountries = $this->getAvailableCountries();
+      if (!empty($fieldValue) && $realFieldName === 'country_id') {
+        if ($this->getAvailableCountries() && empty($this->getAvailableCountries()[$fieldValue])) {
+          // We restrict to allowed countries for address fields - but not custom country fields.
+          $fieldValue = 'invalid_import_value';
+        }
+      }
+
+      // The new way...
+      if (!isset($contactArray[$entity][$entityKey])) {
+        $contactArray[$entity][$entityKey] = $locationValues;
+      }
+      if (!isset($locationValues[$fieldName])) {
+        // These lines add the values to params 'the old way'
+        // The old way is then re-formatted by formatCommonData more
+        // or less as per below.
+        // @todo - stop doing this & remove handling in formatCommonData.
+        $locationValues[$fieldName] = $fieldValue;
+        $contactArray[$fieldName] = (array) ($contactArray[$fieldName] ?? []);
+        $contactArray[$fieldName][$entityKey] = $locationValues;
+        $contactArray[$entity][$entityKey][$realFieldName] = $fieldValue;
+      }
     }
     else {
       $contactArray[$fieldName] = $this->getTransformedFieldValue($fieldName, $importedValue);
index 8d9742b7ce595a0e71cfea511db2e4cb78b1d191..6d8e6e395809425c40510889e5742ef610befc5f 100644 (file)
@@ -1203,7 +1203,7 @@ abstract class CRM_Import_Parser {
         $missingFields[$key] = implode(' ' . ts('and') . ' ', $missing);
       }
     }
-    throw new CRM_Core_Exception(($prefixString ? ($prefixString . ' ') : '') . ts('Missing required fields:') . ' ' . implode(' ' . ts('OR') . ' ', $missingFields));
+    throw new CRM_Core_Exception($prefixString . ts('Missing required fields:') . ' ' . implode(' ' . ts('OR') . ' ', $missingFields));
   }
 
   /**
@@ -1217,8 +1217,9 @@ abstract class CRM_Import_Parser {
    * @throws \API_Exception
    */
   protected function getTransformedFieldValue(string $fieldName, $importedValue) {
+    $transformableFields = array_merge($this->metadataHandledFields, ['country_id']);
     // For now only do gender_id etc as we need to work through removing duplicate handling
-    if (empty($importedValue) || !in_array($fieldName, $this->metadataHandledFields, TRUE)) {
+    if (empty($importedValue) || !in_array($fieldName, $transformableFields, TRUE)) {
       return $importedValue;
     }
     $fieldMetadata = $this->getFieldMetadata($fieldName);
@@ -1233,7 +1234,12 @@ abstract class CRM_Import_Parser {
       $value = CRM_Utils_Date::formatDate($importedValue, $this->getSubmittedValue('dateFormats'));
       return ($value) ?: 'invalid_import_value';
     }
-    return $this->getFieldOptions($fieldName)[is_numeric($importedValue) ? $importedValue : mb_strtolower($importedValue)] ?? 'invalid_import_value';
+    $options = $this->getFieldOptions($fieldName);
+    if ($options !== FALSE) {
+      $comparisonValue = is_numeric($importedValue) ? $importedValue : mb_strtolower($importedValue);
+      return $options[$comparisonValue] ?? 'invalid_import_value';
+    }
+    return $importedValue;
   }
 
   /**
@@ -1262,28 +1268,29 @@ abstract class CRM_Import_Parser {
    * @throws \Civi\API\Exception\NotImplementedException
    */
   protected function getFieldMetadata(string $fieldName, bool $loadOptions = FALSE, $limitToContactType = FALSE): array {
-    $fieldMetadata = $this->getImportableFieldsMetadata()[$fieldName] ?? ($limitToContactType ? NULL : CRM_Contact_BAO_Contact::importableFields('All')[$fieldName]);
+
+    $fieldMap = ['country_id' => 'country'];
+    $fieldMapName = empty($fieldMap[$fieldName]) ? $fieldName : $fieldMap[$fieldName];
+
+    $fieldMetadata = $this->getImportableFieldsMetadata()[$fieldMapName] ?? ($limitToContactType ? NULL : CRM_Contact_BAO_Contact::importableFields('All')[$fieldMapName]);
     if ($loadOptions && !isset($fieldMetadata['options'])) {
-      if (empty($fieldMetadata['pseudoconstant'])) {
-        $this->importableFieldsMetadata[$fieldName]['options'] = FALSE;
-      }
-      else {
-        $options = civicrm_api4($fieldMetadata['entity'], 'getFields', [
-          'loadOptions' => ['id', 'name', 'label'],
-          'where' => [['name', '=', $fieldMetadata['name']]],
-          'select' => ['options'],
-        ])->first()['options'];
-        // We create an array of the possible variants - notably including
-        // name AND label as either might be used. We also lower case before checking
-        $values = [];
-        foreach ($options as $option) {
-          $values[$option['id']] = $option['id'];
-          $values[mb_strtolower($option['name'])] = $option['id'];
-          $values[mb_strtolower($option['label'])] = $option['id'];
-        }
-        $this->importableFieldsMetadata[$fieldName]['options'] = $values;
+
+      $options = civicrm_api4($this->getFieldEntity($fieldName), 'getFields', [
+        'loadOptions' => ['id', 'name', 'label'],
+        'where' => [['name', '=', empty($fieldMap[$fieldName]) ? $fieldMetadata['name'] : $fieldName]],
+        'select' => ['options'],
+      ])->first()['options'];
+      // We create an array of the possible variants - notably including
+      // name AND label as either might be used. We also lower case before checking
+      $values = [];
+      foreach ($options as $option) {
+        $values[$option['id']] = $option['id'];
+        $values[mb_strtolower($option['name'])] = $option['id'];
+        $values[mb_strtolower($option['label'])] = $option['id'];
       }
-      return $this->importableFieldsMetadata[$fieldName];
+
+      $this->importableFieldsMetadata[$fieldMapName]['options'] = $values;
+      return $this->importableFieldsMetadata[$fieldMapName];
     }
     return $fieldMetadata;
   }
index 201a0986982c5a6da7e0198d6ea29f101c574aa2..6259bf4df7394d117a8f6a3012cfc85bf15ef7b2 100644 (file)
@@ -512,10 +512,6 @@ class CRM_Contact_BAO_ContactTest extends CiviUnitTestCase {
     CRM_Contact_BAO_Contact::resolveDefaults($params);
 
     $this->assertEquals(1004, $params['address'][1]['state_province_id']);
-    $this->assertEquals(CRM_Core_PseudoConstant::country($params['address'][1]['country_id']),
-      $params['address'][1]['country'],
-      'Check for country.'
-    );
   }
 
   /**
diff --git a/tests/phpunit/CRM/Contact/Import/Form/data/individual_country_state_county_with_related.csv b/tests/phpunit/CRM/Contact/Import/Form/data/individual_country_state_county_with_related.csv
new file mode 100644 (file)
index 0000000..ef3cf19
--- /dev/null
@@ -0,0 +1,7 @@
+First Name,Last Name,Email,County,Country,State,Custom field state,Custom Field Country,Address Custom Field Country,Address Custom field state,Mum Name,Mum Last name,Mum email,Mum State,Mum Country,Mum County,Address Mum Custom Field Country,Address Mum Custom field state,Mum Custom Field Country,Mum Custom field State,expected,error_value
+Susie,Jones,susie@example.com,,ABC,,,,,,Mum,Jones,mum@example.com,,,,,,,,Invalid,ABC
+Susie,Jones,susie@example.com,,,,,,,,Mum,Jones,mum@example.com,NSW,ABC,,,,,,Invalid,ABC
+Susie,Jones,susie@example.com,,Australia,NSW,NSW,Australia,Australia,NSW,Mum,Jones,mum@example.com,NSW,Australia,,Australia,NSW,Australia,NSW,Valid,
+Susie,Jones,susie@example.com,,AU,New South Wales,New South Wales,AU,AU,New South Wales,Mum,Jones,mum@example.com,New South Wales,AU,,AU,New South Wales,Australia,New South Wales,Valid,
+Susie,Jones,susie@example.com,,1013,New South Wales,,1013,1013,New South Wales,Mum,Jones,mum@example.com,New South Wales,1013,,1013,New South Wales,1013,New South Wales,Valid,
+Susie,Jones,susie@example.com,,AUSTRALIA,,,,,,Mum,Jones,mum@example.com,,austRalia,,,,,,Valid,
index 46e3b3539fc19952cb8a715dc22e4a5c0750d115..130f04df4731c3b1ab4172fb28ff3c08bc24bc73 100644 (file)
 use Civi\Api4\Address;
 use Civi\Api4\Contact;
 use Civi\Api4\ContactType;
+use Civi\Api4\Email;
+use Civi\Api4\IM;
 use Civi\Api4\LocationType;
+use Civi\Api4\OpenID;
+use Civi\Api4\Phone;
 use Civi\Api4\RelationshipType;
 use Civi\Api4\UserJob;
+use Civi\Api4\Website;
 
 /**
  *  Test contact import parser.
@@ -37,6 +42,13 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
    */
   protected $entity = 'Contact';
 
+  /**
+   * Array of existing relationships.
+   *
+   * @var array
+   */
+  private $relationships = [];
+
   /**
    * Tear down after test.
    */
@@ -1049,6 +1061,63 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
     $this->assertCount(8, $contacts);
   }
 
+  /**
+   * Test importing state country & county.
+   *
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   */
+  public function testImportCountryStateCounty(): void {
+    $childKey = $this->getRelationships()['Child of']['id'] . '_a_b';
+    // @todo - rows that don't work yet are set to do_not_import.
+    // $addressCustomGroupID = $this->createCustomGroup(['extends' => 'Address', 'name' => 'Address']);
+    // $contactCustomGroupID = $this->createCustomGroup(['extends' => 'Contact', 'name' => 'Contact']);
+    // $addressCustomFieldID = $this->createCountryCustomField(['custom_group_id' => $addressCustomGroupID])['id'];
+    // $contactCustomFieldID = $this->createMultiCountryCustomField(['custom_group_id' => $contactCustomGroupID])['id'];
+    // $customField = 'custom_' . $contactCustomFieldID;
+    // $addressCustomField = 'custom_' . $addressCustomFieldID;
+
+    $mapper = [
+      ['first_name'],
+      ['last_name'],
+      ['email'],
+      ['county'],
+      ['country'],
+      ['state_province'],
+      // [$customField, 'state_province'],
+      ['do_not_import'],
+      // [$customField, 'country'],
+      ['do_not_import'],
+      // [$addressCustomField, 'country'],
+      ['do_not_import'],
+      // [$addressCustomField, 'state_province'],
+      ['do_not_import'],
+      [$childKey, 'first_name'],
+      [$childKey, 'last_name'],
+      [$childKey, 'email'],
+      [$childKey, 'state_province'],
+      [$childKey, 'country'],
+      [$childKey, 'county'],
+      // [$childKey, $addressCustomField, 'country'],
+      ['do_not_import'],
+      // [$childKey, $addressCustomField, 'state_province'],
+      ['do_not_import'],
+      // [$childKey, $customField, 'country'],
+      ['do_not_import'],
+      // [$childKey, $customField, 'state_province'],
+      ['do_not_import'],
+    ];
+    $csv = 'individual_country_state_county_with_related.csv';
+    $this->validateMultiRowCsv($csv, $mapper, 'error_value');
+
+    $this->importCSV($csv, $mapper);
+    $contacts = $this->getImportedContacts();
+    foreach ($contacts as $contact) {
+      $this->assertEquals(1013, $contact['address'][0]['country_id']);
+    }
+    $this->assertCount(2, $contacts);
+  }
+
   /**
    * Test date validation.
    *
@@ -1152,11 +1221,7 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
    */
   public function testImportLocations(): void {
     $csv = 'individual_locations_with_related.csv';
-    $relationships = (array) RelationshipType::get()->addSelect('name_a_b', 'id')->addWhere('name_a_b', 'IN', [
-      'Child of',
-      'Sibling of',
-      'Employee of',
-    ])->execute()->indexBy('name_a_b');
+    $relationships = $this->getRelationships();
 
     $childKey = $relationships['Child of']['id'] . '_a_b';
     $siblingKey = $relationships['Sibling of']['id'] . '_a_b';
@@ -1335,7 +1400,9 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
   /**
    * CRM-19888 default country should be used if ambigous.
    *
+   * @throws \API_Exception
    * @throws \CRM_Core_Exception
+   * @throws \CiviCRM_API3_Exception
    */
   public function testImportAmbiguousStateCountry(): void {
     $this->callAPISuccess('Setting', 'create', ['defaultContactCountry' => 1228]);
@@ -1501,6 +1568,21 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
     ])->execute();
   }
 
+  /**
+   * @return array
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  private function getRelationships(): array {
+    if (empty($this->relationships)) {
+      $this->relationships = (array) RelationshipType::get()
+        ->addSelect('name_a_b', 'id')
+        ->execute()
+        ->indexBy('name_a_b');
+    }
+    return $this->relationships;
+  }
+
   /**
    * @param array $fields Array of fields to be imported
    * @param array $allfields Array of all fields which can be part of import
@@ -1563,7 +1645,7 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
     $this->assertEquals([
       'first_name' => 'Bob',
       'phone' => [
-        [
+        '1_1' => [
           'phone' => '123',
           'location_type_id' => 1,
           'phone_type_id' => 1,
@@ -1571,32 +1653,34 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
       ],
       '5_a_b' => [
         'contact_type' => 'Organization',
-        'url' =>
-          [
-
-            [
-              'url' => 'https://example.org',
-              'website_type_id' => 1,
-            ],
+        'website' => [
+          'https://example.org' => [
+            'url' => 'https://example.org',
+            'website_type_id' => 1,
           ],
-        'phone' =>
-          [
-            [
-              'phone' => '456',
-              'location_type_id' => 1,
-              'phone_type_id' => 1,
-            ],
+        ],
+        // We still pump out the legacy key too - for now!
+        'url' => [
+          'https://example.org' => [
+            'url' => 'https://example.org',
+            'website_type_id' => 1,
           ],
-      ],
-      'im' =>
-        [
-
-          [
-            'im' => 'my-handle',
+        ],
+        'phone' => [
+          '1_1' => [
+            'phone' => '456',
             'location_type_id' => 1,
-            'provider_id' => 1,
+            'phone_type_id' => 1,
           ],
         ],
+      ],
+      'im' => [
+        '1_1' => [
+          'im' => 'my-handle',
+          'location_type_id' => 1,
+          'provider_id' => 1,
+        ],
+      ],
       'contact_type' => 'Individual',
     ], $params);
   }
@@ -1799,4 +1883,22 @@ class CRM_Contact_Import_Parser_ContactTest extends CiviUnitTestCase {
     }
   }
 
+  /**
+   * Get the contacts we imported (Susie Jones & family).
+   *
+   * @return array
+   * @throws \API_Exception
+   */
+  public function getImportedContacts(): array {
+    return (array) Contact::get()
+      ->addWhere('display_name', 'IN', ['Susie Jones', 'Mum Jones', 'sis@example.com', 'Soccer Superstars'])
+      ->addChain('phone', Phone::get()->addWhere('contact_id', '=', '$id'))
+      ->addChain('address', Address::get()->addWhere('contact_id', '=', '$id'))
+      ->addChain('website', Website::get()->addWhere('contact_id', '=', '$id'))
+      ->addChain('im', IM::get()->addWhere('contact_id', '=', '$id'))
+      ->addChain('email', Email::get()->addWhere('contact_id', '=', '$id'))
+      ->addChain('openid', OpenID::get()->addWhere('contact_id', '=', '$id'))
+      ->execute()->indexBy('display_name');
+  }
+
 }