$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;
//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 {
}
foreach ($params as $key => $value) {
- if ($value === 'invalid_import_value') {
- $errors[] = $this->getFieldMetadata($key)['title'];
- }
if ($value) {
switch ($key) {
}
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) {
$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;
$addressFields = [
'county',
- 'country',
+ 'country_id',
'state_province',
'supplemental_address_1',
'supplemental_address_2',
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
//date-format part ends
- $errorMessage = NULL;
+ $errorMessage = implode(', ', $errors);
//checking error in custom data
$this->isErrorInCustomData($params, $errorMessage, $params['contact_sub_type'] ?? NULL);
}
}
+ /**
+ * 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.
*
*/
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);
$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));
}
/**
* @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);
$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;
}
/**
* @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;
}
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.'
- );
}
/**
--- /dev/null
+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,
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.
*/
protected $entity = 'Contact';
+ /**
+ * Array of existing relationships.
+ *
+ * @var array
+ */
+ private $relationships = [];
+
/**
* Tear down after test.
*/
$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.
*
*/
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';
/**
* 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]);
])->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
$this->assertEquals([
'first_name' => 'Bob',
'phone' => [
- [
+ '1_1' => [
'phone' => '123',
'location_type_id' => 1,
'phone_type_id' => 1,
],
'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);
}
}
}
+ /**
+ * 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');
+ }
+
}