From c25623313f2e016ca443ce188a0339b39c7197e8 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Mon, 18 Apr 2022 16:31:06 +1200 Subject: [PATCH 1/3] [REF] [Import] Strict typing on function that throws an exception if null --- CRM/Import/Forms.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CRM/Import/Forms.php b/CRM/Import/Forms.php index 808149483c38..b7d15b55d0b4 100644 --- a/CRM/Import/Forms.php +++ b/CRM/Import/Forms.php @@ -96,7 +96,7 @@ protected function getDataSources(): array { * * @throws \CRM_Core_Exception */ - protected function getDataSourceClassName(): ?string { + protected function getDataSourceClassName(): string { $className = CRM_Utils_Request::retrieveValue( 'dataSource', 'String' From e36f7ee104aaf480812bcd905deaebfd05766aa7 Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Mon, 18 Apr 2022 16:49:20 +1200 Subject: [PATCH 2/3] Initial permission on UserJob This adds 1) an acl so only the creator can access and 2) permits anyone with access CiviCRM to GET their own jobs 3) permission on CREATE is set to administer civicrm I am thinking that create might be too strict but it might be better to start this way. Likewise we probably want sysadmins to be able to access other people's jobs but unless we have a plan now for what permissions we want we can punt the question by keeping these strict for now --- CRM/Core/BAO/UserJob.php | 16 ++++++++++++++++ CRM/Core/Permission.php | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/CRM/Core/BAO/UserJob.php b/CRM/Core/BAO/UserJob.php index 29e7a698973e..5eaf41979e99 100644 --- a/CRM/Core/BAO/UserJob.php +++ b/CRM/Core/BAO/UserJob.php @@ -20,6 +20,22 @@ */ class CRM_Core_BAO_UserJob extends CRM_Core_DAO_UserJob { + /** + * Restrict access to the relevant user. + * + * Note that it is likely we might want to permit other users such as + * sysadmins to access other people's user_jobs in future but it has been + * kept tightly restricted for initial simplicity (ie do we want to + * use an existing permission? a new permission ? do they require + * 'view all contacts' etc. + * + * @inheritDoc + */ + public function addSelectWhereClause(): array { + $clauses['created_id'] = '= ' . (int) CRM_Core_Session::getLoggedInContactID(); + return $clauses; + } + /** * Get the statuses for Import Jobs. * diff --git a/CRM/Core/Permission.php b/CRM/Core/Permission.php index 8645a836847d..921e59549fd7 100644 --- a/CRM/Core/Permission.php +++ b/CRM/Core/Permission.php @@ -1543,6 +1543,18 @@ public static function getEntityActionPermissions() { $permissions['option_value'] = $permissions['uf_group']; $permissions['option_group'] = $permissions['option_value']; + // User Job permissions - we access these using acls on the get action. + // For create it probably makes sense (at least initially) to be stricter + // as the forms doing the work can set the permission check to FALSE. + $permissions['user_job'] = [ + 'get' => [ + 'access CiviCRM', + ], + 'default' => [ + 'administer CiviCRM', + ], + ]; + $permissions['custom_value'] = [ 'gettree' => ['access CiviCRM'], ]; From 7b057b666202a6548d8900e407a0e17d5ffd174a Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Wed, 20 Apr 2022 07:30:53 +1200 Subject: [PATCH 3/3] Start to use user_job to track import --- CRM/Contact/Import/Form/DataSource.php | 15 +- CRM/Core/BAO/UserJob.php | 7 +- CRM/Import/DataSource.php | 106 +++++++++ CRM/Import/DataSource/CSV.php | 16 +- CRM/Import/DataSource/SQL.php | 10 + CRM/Import/Form/DataSourceConfig.php | 39 ++++ CRM/Import/Forms.php | 216 +++++++++++++++++- CRM/Import/Parser.php | 44 ++++ .../CRM/Contact/Import/Form/DataSource.tpl | 2 +- .../Contact/Import/Form/DataSourceTest.php | 101 +++++++- .../CRM/Contact/Import/Form/data/yogi.csv | 2 + .../phpunit/CRM/Import/DataSource/CsvTest.php | 8 + 12 files changed, 537 insertions(+), 29 deletions(-) create mode 100644 tests/phpunit/CRM/Contact/Import/Form/data/yogi.csv diff --git a/CRM/Contact/Import/Form/DataSource.php b/CRM/Contact/Import/Form/DataSource.php index 0377f23fdf75..d4da8d74ed4b 100644 --- a/CRM/Contact/Import/Form/DataSource.php +++ b/CRM/Contact/Import/Form/DataSource.php @@ -74,7 +74,7 @@ public function preProcess() { public function buildQuickForm() { $this->assign('urlPath', 'civicrm/import/datasource'); - $this->assign('urlPathVar', 'snippet=4'); + $this->assign('urlPathVar', 'snippet=4&user_job_id=' . $this->get('user_job_id')); $this->add('select', 'dataSource', ts('Data Source'), $this->getDataSources(), TRUE, ['onchange' => 'buildDataSourceFormBlock(this.value);'] @@ -168,10 +168,16 @@ public function setDefaultValues() { * Call the DataSource's postProcess method. * * @throws \CRM_Core_Exception + * @throws \API_Exception */ public function postProcess() { $this->controller->resetPage('MapField'); - + if (!$this->getUserJobID()) { + $this->createUserJob(); + } + else { + $this->updateUserJobMetadata('submitted_values', $this->getSubmittedValues()); + } // Setup the params array $this->_params = $this->controller->exportValues($this->_name); @@ -208,6 +214,7 @@ public function postProcess() { $parser = new CRM_Contact_Import_Parser_Contact($mapper); $parser->setMaxLinesToProcess(100); + $parser->setUserJobID($this->getUserJobID()); $parser->run($importTableName, $mapper, CRM_Import_Parser::MODE_MAPFIELD, @@ -234,12 +241,12 @@ public function postProcess() { * @throws \CRM_Core_Exception */ private function instantiateDataSource(): void { - $dataSourceName = $this->getDataSourceClassName(); - $dataSource = new $dataSourceName(); + $dataSource = $this->getDataSourceObject(); // Get the PEAR::DB object $dao = new CRM_Core_DAO(); $db = $dao->getDatabaseConnection(); $dataSource->postProcess($this->_params, $db, $this); + $this->updateUserJobMetadata('DataSource', $dataSource->getDataSourceMetadata()); } /** diff --git a/CRM/Core/BAO/UserJob.php b/CRM/Core/BAO/UserJob.php index 5eaf41979e99..193d7186e09b 100644 --- a/CRM/Core/BAO/UserJob.php +++ b/CRM/Core/BAO/UserJob.php @@ -50,11 +50,16 @@ public static function getStatuses(): array { ], [ 'id' => 2, + 'name' => 'draft', + 'label' => ts('Draft'), + ], + [ + 'id' => 3, 'name' => 'scheduled', 'label' => ts('Scheduled'), ], [ - 'id' => 3, + 'id' => 4, 'name' => 'in_progress', 'label' => ts('In Progress'), ], diff --git a/CRM/Import/DataSource.php b/CRM/Import/DataSource.php index e7a8cc8459d8..c5a4a44cef99 100644 --- a/CRM/Import/DataSource.php +++ b/CRM/Import/DataSource.php @@ -15,12 +15,118 @@ * @copyright CiviCRM LLC https://civicrm.org/licensing */ +use Civi\Api4\UserJob; + /** * This class defines the DataSource interface but must be subclassed to be * useful. */ abstract class CRM_Import_DataSource { + /** + * Class constructor. + * + * @param int|null $userJobID + */ + public function __construct(int $userJobID = NULL) { + if ($userJobID) { + $this->setUserJobID($userJobID); + } + } + + /** + * Form fields declared for this datasource. + * + * @var string[] + */ + protected $submittableFields = []; + + /** + * User job id. + * + * This is the primary key of the civicrm_user_job table which is used to + * track the import. + * + * @var int + */ + protected $userJobID; + + /** + * @return int|null + */ + public function getUserJobID(): ?int { + return $this->userJobID; + } + + /** + * Set user job ID. + * + * @param int $userJobID + */ + public function setUserJobID(int $userJobID): void { + $this->userJobID = $userJobID; + } + + /** + * User job details. + * + * This is the relevant row from civicrm_user_job. + * + * @var array + */ + protected $userJob; + + /** + * Get User Job. + * + * API call to retrieve the userJob row. + * + * @return array + * + * @throws \API_Exception + */ + protected function getUserJob(): array { + if (!$this->userJob) { + $this->userJob = UserJob::get() + ->addWhere('id', '=', $this->getUserJobID()) + ->execute() + ->first(); + } + return $this->userJob; + } + + /** + * Generated metadata relating to the the datasource. + * + * This is values that are computed within the DataSource class and + * which are stored in the userJob metadata in the DataSource key - eg. + * + * ['table_name' => $] + * + * Will be in the user_job.metadata field encoded into the json like + * + * `{'DataSource' : ['table_name' => $], 'submitted_values' : .....}` + * + * @var array + */ + protected $dataSourceMetadata = []; + + /** + * @return array + */ + public function getDataSourceMetadata(): array { + return $this->dataSourceMetadata; + } + + /** + * Get the fields declared for this datasource. + * + * @return string[] + */ + public function getSubmittableFields(): array { + return $this->submittableFields; + } + /** * Provides information about the data source. * diff --git a/CRM/Import/DataSource/CSV.php b/CRM/Import/DataSource/CSV.php index 9e0c83087763..c91ca15a8841 100644 --- a/CRM/Import/DataSource/CSV.php +++ b/CRM/Import/DataSource/CSV.php @@ -18,6 +18,13 @@ class CRM_Import_DataSource_CSV extends CRM_Import_DataSource { const NUM_ROWS_TO_INSERT = 100; + /** + * Form fields declared for this datasource. + * + * @var string[] + */ + protected $submittableFields = ['skipColumnHeader', 'uploadField']; + /** * Provides information about the data source. * @@ -88,8 +95,12 @@ public function postProcess(&$params, &$db, &$form) { CRM_Utils_Array::value('fieldSeparator', $params, ',') ); - $form->set('originalColHeader', CRM_Utils_Array::value('original_col_header', $result)); + $form->set('originalColHeader', CRM_Utils_Array::value('column_headers', $result)); $form->set('importTableName', $result['import_table_name']); + $this->dataSourceMetadata = [ + 'table_name' => $result['import_table_name'], + 'column_headers' => $result['column_headers'] ?? NULL, + ]; } /** @@ -135,7 +146,7 @@ private static function _CsvToTable( // create the column names from the CSV header or as col_0, col_1, etc. if ($headers) { //need to get original headers. - $result['original_col_header'] = $firstrow; + $result['column_headers'] = $firstrow; $strtolower = function_exists('mb_strtolower') ? 'mb_strtolower' : 'strtolower'; $columns = array_map($strtolower, $firstrow); @@ -242,7 +253,6 @@ private static function _CsvToTable( //get the import tmp table name. $result['import_table_name'] = $tableName; - return $result; } diff --git a/CRM/Import/DataSource/SQL.php b/CRM/Import/DataSource/SQL.php index 633b3dd05a1f..2e712f43171e 100644 --- a/CRM/Import/DataSource/SQL.php +++ b/CRM/Import/DataSource/SQL.php @@ -16,6 +16,13 @@ */ class CRM_Import_DataSource_SQL extends CRM_Import_DataSource { + /** + * Form fields declared for this datasource. + * + * @var string[] + */ + protected $submittableFields = ['sqlQuery']; + /** * Provides information about the data source. * @@ -90,6 +97,9 @@ public function postProcess(&$params, &$db, &$form) { ); $form->set('importTableName', $importJob->getTableName()); + $this->dataSourceMetadata = [ + 'table_name' => $importJob->getTableName(), + ]; } } diff --git a/CRM/Import/Form/DataSourceConfig.php b/CRM/Import/Form/DataSourceConfig.php index 93b34e739f99..e5ddfc870bb8 100644 --- a/CRM/Import/Form/DataSourceConfig.php +++ b/CRM/Import/Form/DataSourceConfig.php @@ -29,13 +29,52 @@ public function preProcess(): void { $dataSourcePath = explode('_', $this->getDataSourceClassName()); $templateFile = 'CRM/Contact/Import/Form/' . $dataSourcePath[3] . '.tpl'; $this->assign('dataSourceFormTemplateFile', $templateFile ?? NULL); + if (CRM_Utils_Request::retrieveValue('user_job_id', 'Integer')) { + $this->setUserJobID(CRM_Utils_Request::retrieveValue('user_job_id', 'Integer')); + } } /** * Build the form object. + * + * @throws \CRM_Core_Exception */ public function buildQuickForm(): void { $this->buildDataSourceFields(); } + /** + * Set defaults. + * + * @return array + * + * @throws \API_Exception + * @throws \CRM_Core_Exception + */ + public function setDefaultValues() { + $defaults = []; + if ($this->userJobID) { + foreach ($this->getDataSourceFields() as $fieldName) { + $defaults[$fieldName] = $this->getSubmittedValue($fieldName); + } + } + return $defaults; + } + + /** + * Get the submitted value, as saved in the user job. + * + * This form is not in the same flow as the DataSource but + * the value we want is saved to the userJob so load it from there. + * + * @param string $fieldName + * + * @return mixed|null + * @throws \API_Exception + */ + public function getSubmittedValue(string $fieldName) { + $userJob = $this->getUserJob(); + return $userJob['metadata']['submitted_values'][$fieldName]; + } + } diff --git a/CRM/Import/Forms.php b/CRM/Import/Forms.php index b7d15b55d0b4..be62a5200e2c 100644 --- a/CRM/Import/Forms.php +++ b/CRM/Import/Forms.php @@ -15,27 +15,116 @@ * @copyright CiviCRM LLC https://civicrm.org/licensing */ +use Civi\Api4\UserJob; + /** * This class helps the forms within the import flow access submitted & parsed values. */ class CRM_Import_Forms extends CRM_Core_Form { /** - * Get the submitted value, accessing it from whatever form in the flow it is submitted on. + * User job id. + * + * This is the primary key of the civicrm_user_job table which is used to + * track the import. + * + * @var int + */ + protected $userJobID; + + /** + * @return int|null + */ + public function getUserJobID(): ?int { + if (!$this->userJobID && $this->get('user_job_id')) { + $this->userJobID = $this->get('user_job_id'); + } + return $this->userJobID; + } + + /** + * Set user job ID. + * + * @param int $userJobID + */ + public function setUserJobID(int $userJobID): void { + $this->userJobID = $userJobID; + // This set allows other forms in the flow ot use $this->get('user_job_id'). + $this->set('user_job_id', $userJobID); + } + + /** + * User job details. + * + * This is the relevant row from civicrm_user_job. + * + * @var array + */ + protected $userJob; + + /** + * Get User Job. + * + * API call to retrieve the userJob row. + * + * @return array + * + * @throws \API_Exception + */ + protected function getUserJob(): array { + if (!$this->userJob) { + $this->userJob = UserJob::get() + ->addWhere('id', '=', $this->getUserJobID()) + ->execute() + ->first(); + } + return $this->userJob; + } + + /** + * Get submitted values stored in the user job. + * + * @return array + * @throws \API_Exception + */ + protected function getUserJobSubmittedValues(): array { + return $this->getUserJob()['metadata']['submitted_values']; + } + + /** + * Fields that may be submitted on any form in the flow. + * + * @var string[] + */ + protected $submittableFields = [ + // Skip column header is actually a field that would be added from the + // datasource - but currently only in contact, it is always there for + // other imports, ditto uploadFile. + 'skipColumnHeader' => 'DataSource', + 'fieldSeparator' => 'DataSource', + 'uploadFile' => 'DataSource', + 'contactType' => 'DataSource', + 'dateFormats' => 'DataSource', + 'savedMapping' => 'DataSource', + 'dataSource' => 'DataSource', + ]; + + /** + * Get the submitted value, accessing it from whatever form in the flow it is + * submitted on. + * * @param string $fieldName * * @return mixed|null + * @throws \CRM_Core_Exception */ public function getSubmittedValue(string $fieldName) { - $mappedValues = [ - 'skipColumnHeader' => 'DataSource', - 'fieldSeparator' => 'DataSource', - 'uploadFile' => 'DataSource', - 'contactType' => 'DataSource', - 'dateFormats' => 'DataSource', - 'savedMapping' => 'DataSource', - 'dataSource' => 'DataSource', - ]; + if ($fieldName === 'dataSource') { + // Hard-coded handling for DataSource as it affects the contents of + // getSubmittableFields and can cause a loop. + return $this->controller->exportValue('DataSource', 'dataSource'); + } + $mappedValues = $this->getSubmittableFields(); if (array_key_exists($fieldName, $mappedValues)) { return $this->controller->exportValue($mappedValues[$fieldName], $fieldName); } @@ -43,6 +132,19 @@ public function getSubmittedValue(string $fieldName) { } + /** + * Get values submitted on any form in the multi-page import flow. + * + * @return array + */ + public function getSubmittedValues(): array { + $values = []; + foreach (array_keys($this->getSubmittableFields()) as $key) { + $values[$key] = $this->getSubmittedValue($key); + } + return $values; + } + /** * Get the available datasource. * @@ -123,11 +225,45 @@ protected function getDataSourceClassName(): string { * @throws \CRM_Core_Exception */ protected function buildDataSourceFields(): void { + $dataSourceClass = $this->getDataSourceObject(); + if ($dataSourceClass) { + $dataSourceClass->buildQuickForm($this); + } + } + + /** + * Get the relevant datasource object. + * + * @return \CRM_Import_DataSource|null + * + * @throws \CRM_Core_Exception + */ + protected function getDataSourceObject(): ?CRM_Import_DataSource { + $className = $this->getDataSourceClassName(); + if ($className) { + /* @var CRM_Import_DataSource $dataSource */ + return new $className($this->getUserJobID()); + } + return NULL; + } + + /** + * Allow the datasource class to add fields. + * + * This is called as a snippet in DataSourceConfig and + * also from DataSource::buildForm to add the fields such + * that quick form picks them up. + * + * @throws \CRM_Core_Exception + */ + protected function getDataSourceFields(): array { $className = $this->getDataSourceClassName(); if ($className) { + /* @var CRM_Import_DataSource $dataSourceClass */ $dataSourceClass = new $className(); - $dataSourceClass->buildQuickForm($this); + return $dataSourceClass->getSubmittableFields(); } + return []; } /** @@ -139,4 +275,62 @@ protected function getDefaultDataSource(): string { return 'CRM_Import_DataSource_CSV'; } + /** + * Get the fields that can be submitted in the Import form flow. + * + * These could be on any form in the flow & are accessed the same way from + * all forms. + * + * @return string[] + * @throws \CRM_Core_Exception + */ + protected function getSubmittableFields(): array { + $dataSourceFields = array_fill_keys($this->getDataSourceFields(), 'DataSource'); + return array_merge($this->submittableFields, $dataSourceFields); + } + + /** + * Create a user job to track the import. + * + * @return int + * + * @throws \API_Exception + */ + protected function createUserJob(): int { + $id = UserJob::create(FALSE) + ->setValues([ + 'created_id' => CRM_Core_Session::getLoggedInContactID(), + 'type_id:name' => 'contact_import', + 'status_id:name' => 'draft', + // This suggests the data could be cleaned up after this. + 'expires_date' => '+ 1 week', + 'metadata' => [ + 'submitted_values' => $this->getSubmittedValues(), + ], + ]) + ->execute() + ->first()['id']; + $this->setUserJobID($id); + return $id; + } + + /** + * @param string $key + * @param array $data + * + * @throws \API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function updateUserJobMetadata(string $key, array $data): void { + $metaData = array_merge( + $this->getUserJob()['metadata'], + [$key => $data] + ); + UserJob::update(FALSE) + ->addWhere('id', '=', $this->getUserJobID()) + ->setValues(['metadata' => $metaData]) + ->execute(); + $this->userJob['metadata'] = $metaData; + } + } diff --git a/CRM/Import/Parser.php b/CRM/Import/Parser.php index c88dc02f8ac3..bd74b492b4dc 100644 --- a/CRM/Import/Parser.php +++ b/CRM/Import/Parser.php @@ -9,6 +9,8 @@ +--------------------------------------------------------------------+ */ +use Civi\Api4\UserJob; + /** * * @package CRM @@ -41,6 +43,48 @@ abstract class CRM_Import_Parser { const CONTACT_INDIVIDUAL = 1, CONTACT_HOUSEHOLD = 2, CONTACT_ORGANIZATION = 4; + /** + * User job id. + * + * This is the primary key of the civicrm_user_job table which is used to + * track the import. + * + * @var int + */ + protected $userJobID; + + /** + * @return int|null + */ + public function getUserJobID(): ?int { + return $this->userJobID; + } + + /** + * Set user job ID. + * + * @param int $userJobID + */ + public function setUserJobID(int $userJobID): void { + $this->userJobID = $userJobID; + } + + /** + * Get User Job. + * + * API call to retrieve the userJob row. + * + * @return array + * + * @throws \API_Exception + */ + protected function getUserJob(): array { + return UserJob::get() + ->addWhere('id', '=', $this->getUserJobID()) + ->execute() + ->first(); + } + /** * Total number of non empty lines * @var int diff --git a/templates/CRM/Contact/Import/Form/DataSource.tpl b/templates/CRM/Contact/Import/Form/DataSource.tpl index deaef9695478..58e4d29f85ff 100644 --- a/templates/CRM/Contact/Import/Form/DataSource.tpl +++ b/templates/CRM/Contact/Import/Form/DataSource.tpl @@ -98,7 +98,7 @@ function buildDataSourceFormBlock(dataSource) { - var dataUrl = {/literal}"{crmURL p=$urlPath h=0 q=$urlPathVar}"{literal}; + var dataUrl = {/literal}"{crmURL p=$urlPath h=0 q=$urlPathVar|smarty:nodefaults}"{literal}; if (!dataSource ) { var dataSource = cj("#dataSource").val(); diff --git a/tests/phpunit/CRM/Contact/Import/Form/DataSourceTest.php b/tests/phpunit/CRM/Contact/Import/Form/DataSourceTest.php index 68056c7e2d97..2d096185abe9 100644 --- a/tests/phpunit/CRM/Contact/Import/Form/DataSourceTest.php +++ b/tests/phpunit/CRM/Contact/Import/Form/DataSourceTest.php @@ -14,6 +14,8 @@ * File for the CRM_Contact_Import_Form_DataSourceTest class. */ +use Civi\Api4\UserJob; + /** * Test contact import datasource. * @@ -22,12 +24,20 @@ */ class CRM_Contact_Import_Form_DataSourceTest extends CiviUnitTestCase { + /** + * Post test cleanup. + */ + public function tearDown(): void { + $this->quickCleanup(['civicrm_user_job']); + parent::tearDown(); + } + /** * Test the form loads without error / notice and mappings are assigned. * * (Added in conjunction with fixed noting on mapping assignment). */ - public function testBuildForm() { + public function testBuildForm(): void { $this->callAPISuccess('Mapping', 'create', ['name' => 'Well dressed ducks', 'mapping_type_id' => 'Import Contact']); $form = $this->getFormObject('CRM_Contact_Import_Form_DataSource'); $form->buildQuickForm(); @@ -35,16 +45,89 @@ public function testBuildForm() { } /** - * Check for (lack of) sql errors on sql import post process. + * Test sql and csv data-sources load and save user jobs. + * + * This test mimics a scenario where the form is submitted more than once + * and the user_job is updated to reflect the new data source. + * + * @throws \API_Exception + * @throws \CRM_Core_Exception + * @throws \Civi\API\Exception\UnauthorizedException */ - public function testSQLSource() { + public function testDataSources(): void { + $this->createLoggedInUser(); $this->callAPISuccess('Mapping', 'create', ['name' => 'Well dressed ducks', 'mapping_type_id' => 'Import Contact']); - /** @var CRM_Import_DataSource_SQL $form */ - $form = $this->getFormObject('CRM_Import_DataSource_SQL', [], 'SQL'); - $coreForm = $this->getFormObject('CRM_Core_Form'); - $db = NULL; - $params = ['sqlQuery' => 'SELECT 1 as id']; - $form->postProcess($params, $db, $coreForm); + + $sqlFormValues = [ + 'dataSource' => 'CRM_Import_DataSource_SQL', + 'sqlQuery' => 'SELECT "bob" as first_name FROM civicrm_option_value LIMIT 5', + ]; + $form = $this->submitDataSourceForm($sqlFormValues); + $userJobID = $form->getUserJobID(); + // Load the user job, using TRUE so permissions apply. + $userJob = UserJob::get(TRUE) + ->addWhere('id', '=', $userJobID) + ->addSelect('metadata') + ->execute()->first(); + // Submitted values should be stored in the user job. + // There are some null values in the submitted_values array - we can + // filter these out as we have not passed in all possible values. + $this->assertEquals($sqlFormValues, array_filter($userJob['metadata']['submitted_values'])); + + // The user job holds the name of the table - which should have 5 rows of bob. + $this->assertNotEmpty($userJob['metadata']['DataSource']['table_name']); + $sqlTableName = $userJob['metadata']['DataSource']['table_name']; + $this->assertEquals(5, CRM_Core_DAO::singleValueQuery( + 'SELECT count(*) FROM ' . $sqlTableName + . " WHERE first_name = 'Bob'" + )); + + // Now we imitate the scenario where the user goes back and + // re-submits the form selecting the csv datasource. + $csvFormValues = [ + 'dataSource' => 'CRM_Import_DataSource_CSV', + 'skipColumnHeader' => 1, + 'uploadFile' => [ + 'name' => __DIR__ . '/data/yogi.csv', + 'type' => 'text/csv', + ], + ]; + // Mimic form re-submission with new values. + $_SESSION['_' . $form->controller->_name . '_container']['values']['DataSource'] = $csvFormValues; + $form->buildForm(); + $form->postProcess(); + // The user job id should not have changed. + $this->assertEquals($userJobID, $form->getUserJobID()); + + $userJob = UserJob::get(TRUE) + ->addWhere('id', '=', $form->getUserJobID()) + ->addSelect('metadata') + ->execute()->first(); + // Submitted values should be updated in the user job. + $this->assertEquals($csvFormValues, array_filter($userJob['metadata']['submitted_values'])); + + $csvTableName = $userJob['metadata']['DataSource']['table_name']; + $this->assertEquals(1, CRM_Core_DAO::singleValueQuery( + 'SELECT count(*) FROM ' . $csvTableName + . " WHERE first_name = 'yogi'" + )); + } + + /** + * Submit the dataSoure form with the provided form values. + * + * @param array $sqlFormValues + * + * @return CRM_Contact_Import_Form_DataSource + * @throws \API_Exception + * @throws \CRM_Core_Exception + */ + private function submitDataSourceForm(array $sqlFormValues): CRM_Contact_Import_Form_DataSource { + /** @var CRM_Contact_Import_Form_DataSource $form */ + $form = $this->getFormObject('CRM_Contact_Import_Form_DataSource', $sqlFormValues); + $form->buildForm(); + $form->postProcess(); + return $form; } } diff --git a/tests/phpunit/CRM/Contact/Import/Form/data/yogi.csv b/tests/phpunit/CRM/Contact/Import/Form/data/yogi.csv new file mode 100644 index 000000000000..a8ccf3f83237 --- /dev/null +++ b/tests/phpunit/CRM/Contact/Import/Form/data/yogi.csv @@ -0,0 +1,2 @@ +Last Name,email,First Name +Bear ,yogi@yellowstone.park ,Yogi diff --git a/tests/phpunit/CRM/Import/DataSource/CsvTest.php b/tests/phpunit/CRM/Import/DataSource/CsvTest.php index 41626941ee8e..a549a1bd076d 100644 --- a/tests/phpunit/CRM/Import/DataSource/CsvTest.php +++ b/tests/phpunit/CRM/Import/DataSource/CsvTest.php @@ -14,6 +14,14 @@ */ class CRM_Import_DataSource_CsvTest extends CiviUnitTestCase { + /** + * Prepare for tests. + */ + public function setUp(): void { + $this->createLoggedInUser(); + parent::setUp(); + } + /** * Test the to csv function. *