From 92bde46aaf777d906a51b359ec02fa78256d5147 Mon Sep 17 00:00:00 2001 From: eileen Date: Mon, 19 Aug 2019 13:01:47 +1200 Subject: [PATCH] Add ImportProcessor class & unit tests + extend existing cover This adds the full ImportProcessor class from the WIP pr #15034 - minus the functions that replace loadSavedMapping) along with tests. It does NOT start to use the new class as yet. Note that #15068 will need rebasing if this is merged first & vice versa The goal is to bring it into use along with extending tests but this is 'safe' in that no funcitonal code is changed --- CRM/Import/ImportProcessor.php | 366 +++++++++++++++++- .../CRM/Contact/Import/Form/MapFieldTest.php | 138 ++++++- 2 files changed, 492 insertions(+), 12 deletions(-) diff --git a/CRM/Import/ImportProcessor.php b/CRM/Import/ImportProcessor.php index ffd38c5317..06b71ace9b 100644 --- a/CRM/Import/ImportProcessor.php +++ b/CRM/Import/ImportProcessor.php @@ -19,6 +19,18 @@ class CRM_Import_ImportProcessor { */ protected $mappingFields = []; + /** + * @var array + */ + protected $metadata = []; + + /** + * Metadata keyed by field title. + * + * @var array + */ + protected $metadataByTitle = []; + /** * Get contact type being imported. * @@ -26,6 +38,137 @@ class CRM_Import_ImportProcessor { */ protected $contactType; + /** + * Get contact sub type being imported. + * + * @var string + */ + protected $contactSubType; + + /** + * Array of valid relationships for the contact type & subtype. + * + * @var array + */ + protected $validRelationships = []; + + /** + * Name of the form. + * + * Used for js for quick form. + * + * @var string + */ + protected $formName; + + /** + * @return string + */ + public function getFormName(): string { + return $this->formName; + } + + /** + * @param string $formName + */ + public function setFormName(string $formName) { + $this->formName = $formName; + } + + /** + * @return array + */ + public function getValidRelationships(): array { + if (!isset($this->validRelationships[$this->getContactType() . '_' . $this->getContactSubType()])) { + //Relationship importables + $relations = CRM_Contact_BAO_Relationship::getContactRelationshipType( + NULL, NULL, NULL, $this->getContactType(), + FALSE, 'label', TRUE, $this->getContactSubType() + ); + asort($relations); + $this->setValidRelationships($relations); + } + return $this->validRelationships[$this->getContactType() . '_' . $this->getContactSubType()]; + } + + /** + * @param array $validRelationships + */ + public function setValidRelationships(array $validRelationships) { + $this->validRelationships[$this->getContactType() . '_' . $this->getContactSubType()] = $validRelationships; + } + + /** + * Get contact subtype for import. + * + * @return string + */ + public function getContactSubType(): string { + return $this->contactSubType; + } + + /** + * Set contact subtype for import. + * + * @param string $contactSubType + */ + public function setContactSubType(string $contactSubType) { + $this->contactSubType = $contactSubType; + } + + /** + * Saved Mapping ID. + * + * @var int + */ + protected $mappingID; + + /** + * @return array + */ + public function getMetadata(): array { + return $this->metadata; + } + + /** + * Setting for metadata. + * + * We wrangle the label for custom fields to include the label since the + * metadata trait presents it in a more 'pure' form but the label is appended for importing. + * + * @param array $metadata + * + * @throws \CiviCRM_API3_Exception + */ + public function setMetadata(array $metadata) { + $fieldDetails = civicrm_api3('CustomField', 'get', [ + 'return' => ['custom_group_id.title'], + 'options' => ['limit' => 0], + ])['values']; + foreach ($metadata as $index => $field) { + if (!empty($field['custom_field_id'])) { + // The 'label' format for import is custom group title :: custom name title + $metadata[$index]['name'] = $index; + $metadata[$index]['title'] .= ' :: ' . $fieldDetails[$field['custom_field_id']]['custom_group_id.title']; + } + } + $this->metadata = $metadata; + } + + /** + * @return int + */ + public function getMappingID(): int { + return $this->mappingID; + } + + /** + * @param int $mappingID + */ + public function setMappingID(int $mappingID) { + $this->mappingID = $mappingID; + } + /** * @return string */ @@ -41,9 +184,30 @@ class CRM_Import_ImportProcessor { } /** + * Set the contact type according to the constant. + * + * @param int $contactTypeKey + */ + public function setContactTypeByConstant($contactTypeKey) { + $constantTypeMap = [ + CRM_Import_Parser::CONTACT_INDIVIDUAL => 'Individual', + CRM_Import_Parser::CONTACT_HOUSEHOLD => 'Household', + CRM_Import_Parser::CONTACT_ORGANIZATION => 'Organization', + ]; + $this->contactType = $constantTypeMap[$contactTypeKey]; + } + + /** + * Get Mapping Fields. + * * @return array + * + * @throws \CiviCRM_API3_Exception */ public function getMappingFields(): array { + if (empty($this->mappingFields) && !empty($this->getMappingID())) { + $this->loadSavedMapping(); + } return $this->mappingFields; } @@ -51,20 +215,130 @@ class CRM_Import_ImportProcessor { * @param array $mappingFields */ public function setMappingFields(array $mappingFields) { - $this->mappingFields = CRM_Utils_Array::rekey($mappingFields, 'column_number'); - ksort($this->mappingFields); - $this->mappingFields = array_values($this->mappingFields); + $this->mappingFields = $this->rekeyBySortedColumnNumbers($mappingFields); } /** * Get the names of the mapped fields. + * + * @throws \CiviCRM_API3_Exception */ public function getFieldNames() { return CRM_Utils_Array::collect('name', $this->getMappingFields()); } + /** + * Get the field name for the given column. + * + * @param int $columnNumber + * + * @return string + * @throws \CiviCRM_API3_Exception + */ + public function getFieldName($columnNumber) { + return $this->getFieldNames()[$columnNumber]; + } + + /** + * Get the field name for the given column. + * + * @param int $columnNumber + * + * @return string + * @throws \CiviCRM_API3_Exception + */ + public function getRelationshipKey($columnNumber) { + $field = $this->getMappingFields()[$columnNumber]; + return empty($field['relationship_type_id']) ? NULL : $field['relationship_type_id'] . '_' . $field['relationship_direction']; + } + + /** + * Get relationship key only if it is valid. + * + * @param int $columnNumber + * + * @return string|null + * + * @throws \CiviCRM_API3_Exception + */ + public function getValidRelationshipKey($columnNumber) { + $key = $this->getRelationshipKey($columnNumber); + return $this->isValidRelationshipKey($key) ? $key : NULL; + } + + /** + * Get the IM Provider ID. + * + * @param int $columnNumber + * + * @return int + * + * @throws \CiviCRM_API3_Exception + */ + public function getIMProviderID($columnNumber) { + return $this->getMappingFields()[$columnNumber]['im_provider_id'] ?? NULL; + } + + /** + * Get the Phone Type + * + * @param int $columnNumber + * + * @return int + * + * @throws \CiviCRM_API3_Exception + */ + public function getPhoneTypeID($columnNumber) { + return $this->getMappingFields()[$columnNumber]['phone_type_id'] ?? NULL; + } + + /** + * Get the Website Type + * + * @param int $columnNumber + * + * @return int + * + * @throws \CiviCRM_API3_Exception + */ + public function getWebsiteTypeID($columnNumber) { + return $this->getMappingFields()[$columnNumber]['website_type_id'] ?? NULL; + } + + /** + * Get the Location Type + * + * Returning 0 rather than null is historical. + * + * @param int $columnNumber + * + * @return int + * + * @throws \CiviCRM_API3_Exception + */ + public function getLocationTypeID($columnNumber) { + return $this->getMappingFields()[$columnNumber]['location_type_id'] ?? 0; + } + + /** + * Get the IM or Phone type. + * + * We have a field that would be the 'relevant' type - which could be either. + * + * @param int $columnNumber + * + * @return int + * + * @throws \CiviCRM_API3_Exception + */ + public function getPhoneOrIMTypeID($columnNumber) { + return $this->getIMProviderID($columnNumber) ?? $this->getPhoneTypeID($columnNumber); + } + /** * Get the location types of the mapped fields. + * + * @throws \CiviCRM_API3_Exception */ public function getFieldLocationTypes() { return CRM_Utils_Array::collect('location_type_id', $this->getMappingFields()); @@ -72,6 +346,8 @@ class CRM_Import_ImportProcessor { /** * Get the phone types of the mapped fields. + * + * @throws \CiviCRM_API3_Exception */ public function getFieldPhoneTypes() { return CRM_Utils_Array::collect('phone_type_id', $this->getMappingFields()); @@ -79,6 +355,8 @@ class CRM_Import_ImportProcessor { /** * Get the names of the im_provider fields. + * + * @throws \CiviCRM_API3_Exception */ public function getFieldIMProviderTypes() { return CRM_Utils_Array::collect('im_provider_id', $this->getMappingFields()); @@ -86,6 +364,8 @@ class CRM_Import_ImportProcessor { /** * Get the names of the website fields. + * + * @throws \CiviCRM_API3_Exception */ public function getFieldWebsiteTypes() { return CRM_Utils_Array::collect('im_provider_id', $this->getMappingFields()); @@ -95,6 +375,8 @@ class CRM_Import_ImportProcessor { * Get an instance of the importer object. * * @return CRM_Contact_Import_Parser_Contact + * + * @throws \CiviCRM_API3_Exception */ public function getImporterObject() { $importer = new CRM_Contact_Import_Parser_Contact( @@ -118,4 +400,82 @@ class CRM_Import_ImportProcessor { return $importer; } + /** + * Load the mapping from the datbase into the format that would be received from the UI. + * + * @throws \CiviCRM_API3_Exception + */ + protected function loadSavedMapping() { + $fields = civicrm_api3('MappingField', 'get', [ + 'mapping_id' => $this->getMappingID(), + 'options' => ['limit' => 0], + ])['values']; + foreach ($fields as $index => $field) { + // Fix up the fact that for lost reasons we save by label not name. + $fields[$index]['label'] = $field['name']; + if (empty($field['relationship_type_id'])) { + $fields[$index]['name'] = $this->getNameFromLabel($field['name']); + } + else { + // Honour legacy chaos factor. + $fields[$index]['name'] = strtolower(str_replace(" ", "_", $field['name'])); + // fix for edge cases, CRM-4954 + if ($fields[$index]['name'] === 'image_url') { + $fields[$index]['name'] = str_replace('url', 'URL', $fields[$index]['name']); + } + } + $fieldSpec = $this->getMetadata()[$fields[$index]['name']]; + if (empty($field['location_type_id']) && !empty($fieldSpec['hasLocationType'])) { + $fields[$index]['location_type_id'] = 'Primary'; + } + } + $this->mappingFields = $this->rekeyBySortedColumnNumbers($fields); + } + + /** + * Get the titles from metadata. + */ + public function getMetadataTitles() { + if (empty($this->metadataByTitle)) { + $this->metadataByTitle = CRM_Utils_Array::collect('title', $this->getMetadata()); + } + return $this->metadataByTitle; + } + + /** + * Rekey the array by the column_number. + * + * @param array $mappingFields + * + * @return array + */ + protected function rekeyBySortedColumnNumbers(array $mappingFields) { + $this->mappingFields = CRM_Utils_Array::rekey($mappingFields, 'column_number'); + ksort($this->mappingFields); + return array_values($this->mappingFields); + } + + /** + * Get the field name from the label. + * + * @param string $label + * + * @return string + */ + protected function getNameFromLabel($label) { + $titleMap = array_flip($this->getMetadataTitles()); + return $titleMap[$label] ?? ''; + } + + /** + * Validate the key against the relationships available for the contatct type & subtype. + * + * @param string $key + * + * @return bool + */ + protected function isValidRelationshipKey($key) { + return !empty($this->getValidRelationships()[$key]) ? TRUE : FALSE; + } + } diff --git a/tests/phpunit/CRM/Contact/Import/Form/MapFieldTest.php b/tests/phpunit/CRM/Contact/Import/Form/MapFieldTest.php index 2602ec9720..9ca03adfa1 100644 --- a/tests/phpunit/CRM/Contact/Import/Form/MapFieldTest.php +++ b/tests/phpunit/CRM/Contact/Import/Form/MapFieldTest.php @@ -38,6 +38,16 @@ */ class CRM_Contact_Import_Form_MapFieldTest extends CiviUnitTestCase { + use CRM_Contact_Import_MetadataTrait; + use CRMTraits_Custom_CustomDataTrait; + + /** + * Map field form. + * + * @var CRM_Contact_Import_Form_MapField + */ + protected $form; + /** * Test the form loads without error / notice and mappings are assigned. * @@ -161,16 +171,113 @@ class CRM_Contact_Import_Form_MapFieldTest extends CiviUnitTestCase { * @throws \CiviCRM_API3_Exception */ public function testLoadSavedMapping($fieldSpec, $expectedJS) { - $form = $this->getFormObject('CRM_Contact_Import_Form_MapField'); - /* @var CRM_Contact_Import_Form_MapField $form */ - $form->set('contactType', CRM_Import_Parser::CONTACT_INDIVIDUAL); + $this->setUpMapFieldForm(); $mapping = $this->callAPISuccess('Mapping', 'create', ['name' => 'my test']); - $this->callAPISuccess('MappingField', 'create', array_merge(['mapping_id' => $mapping], $fieldSpec)); - $result = $this->loadSavedMapping($form, $mapping['id'], 1); + $this->callAPISuccess('MappingField', 'create', array_merge(['mapping_id' => $mapping['id']], $fieldSpec)); + $result = $this->loadSavedMapping($this->form, $mapping['id'], $fieldSpec['column_number']); $this->assertEquals($expectedJS, $result['js']); } + /** + * Tests the 'final' methods for loading the direct mapping. + * + * In conjunction with testing our existing function this tests the methods we want to migrate to + * to clean it up. + * + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception + */ + public function testLoadSavedMappingDirect() { + $this->entity = 'Contact'; + $this->createCustomGroupWithFieldOfType(['title' => 'My Field']); + $this->setUpMapFieldForm(); + $mapping = $this->callAPISuccess('Mapping', 'create', ['name' => 'my test', 'label' => 'Special custom']); + foreach ([ + [ + 'name' => 'Addressee', + 'column_number' => '0', + ], + [ + 'name' => 'Postal Greeting', + 'column_number' => '1', + ], + [ + 'name' => 'Phone', + 'column_number' => '2', + 'location_type_id' => '1', + 'phone_type_id' => '1', + ], + [ + 'name' => 'Street Address', + 'column_number' => '3', + ], + [ + 'name' => 'Enter text here :: My Field', + 'column_number' => '4', + ], + [ + 'name' => 'Street Address', + 'column_number' => '5', + 'location_type_id' => '1', + ], + [ + 'name' => 'City', + 'column_number' => '6', + 'location_type_id' => '1', + ], + [ + 'name' => 'State Province', + 'column_number' => '7', + 'relationship_type_id' => 4, + 'relationship_direction' => 'a_b', + 'location_type_id' => '1', + ], + [ + 'name' => 'Url', + 'column_number' => '8', + 'relationship_type_id' => 4, + 'relationship_direction' => 'a_b', + 'website_type_id' => 2, + ], + [ + 'name' => 'Phone', + 'column_number' => '9', + 'relationship_type_id' => 4, + 'location_type_id' => '1', + 'relationship_direction' => 'a_b', + 'phone_type_id' => 2, + ], + [ + 'name' => 'Phone', + 'column_number' => '10', + 'location_type_id' => '1', + 'phone_type_id' => '3', + ], + ] as $mappingField) { + $this->callAPISuccess('MappingField', 'create', array_merge([ + 'mapping_id' => $mapping['id'], + 'grouping' => 1, + 'contact_type' => 'Individual', + ], $mappingField)); + } + $processor = new CRM_Import_ImportProcessor(); + $processor->setMappingID($mapping['id']); + $processor->setMetadata($this->getContactImportMetadata()); + $this->assertEquals(3, $processor->getPhoneOrIMTypeID(10)); + $this->assertEquals(3, $processor->getPhoneTypeID(10)); + $this->assertEquals(1, $processor->getLocationTypeID(10)); + $this->assertEquals(2, $processor->getWebsiteTypeID(8)); + $this->assertEquals('4_a_b', $processor->getRelationshipKey(9)); + $this->assertEquals('addressee', $processor->getFieldName(0)); + $this->assertEquals('street_address', $processor->getFieldName(3)); + $this->assertEquals($this->getCustomFieldName('text'), $processor->getFieldName(4)); + $this->assertEquals('url', $processor->getFieldName(8)); + + $processor->setContactTypeByConstant(CRM_Import_Parser::CONTACT_HOUSEHOLD); + $this->assertEquals('Household', $processor->getContactType()); + } + /** * Get data for map field tests. */ @@ -178,10 +285,13 @@ class CRM_Contact_Import_Form_MapFieldTest extends CiviUnitTestCase { return [ [ ['name' => 'First Name', 'contact_type' => 'Individual', 'column_number' => 1], - 'document.forms.MapField[\'mapper[1][1]\'].style.display = \'none\'; -document.forms.MapField[\'mapper[1][2]\'].style.display = \'none\'; -document.forms.MapField[\'mapper[1][3]\'].style.display = \'none\'; -', + "document.forms.MapField['mapper[1][1]'].style.display = 'none'; +document.forms.MapField['mapper[1][2]'].style.display = 'none'; +document.forms.MapField['mapper[1][3]'].style.display = 'none';\n", + ], + [ + ['name' => 'Phone', 'contact_type' => 'Individual', 'column_number' => 8, 'phone_type_id' => 1, 'location_type_id' => 2], + "document.forms.MapField['mapper[8][3]'].style.display = 'none';\n", ], ]; } @@ -222,4 +332,14 @@ document.forms.MapField[\'mapper[1][3]\'].style.display = \'none\'; return ['defaults' => $return[0], 'js' => $return[1]]; } + /** + * Set up the mapping form. + * + * @throws \CRM_Core_Exception + */ + private function setUpMapFieldForm() { + $this->form = $this->getFormObject('CRM_Contact_Import_Form_MapField'); + $this->form->set('contactType', CRM_Import_Parser::CONTACT_INDIVIDUAL); + } + } -- 2.25.1