The DataSource screen currently declares the type so this makes it available on MapField
Minor tidy up/ standardisation on Contact defaultValues
This also allows the parent to override the values, e.g if loaded from a template
Fix BAO to support Template UserJobs
- do not save non-template fields
- delete templates on Mapping deletes
Add support for UserJob templates at the form level
Fix for strict typing
* @return array
* reference to the array of default values
*/
- public function setDefaultValues() {
- $defaults = parent::setDefaultValues();
- $defaults['contactType'] = 'Individual';
- $defaults['disableUSPS'] = TRUE;
-
- if ($this->get('loadedMapping')) {
- $defaults['savedMapping'] = $this->get('loadedMapping');
- }
-
- return $defaults;
- }
-
- /**
- * Call the DataSource's postProcess method.
- *
- * @throws \CRM_Core_Exception
- */
- public function postProcess() {
- $this->controller->resetPage('MapField');
- $this->processDatasource();
- // @todo - this params are being set here because they were / possibly still
- // are in some places being accessed by forms later in the flow
- // ie CRM_Contact_Import_Form_MapField, CRM_Contact_Import_Form_Preview
- // which was the old way of saving values submitted on this form such that
- // the other forms could access them. Now they should use
- // `getSubmittedValue` or simply not get them if the only
- // reason is to pass to the Parser which can itself
- // call 'getSubmittedValue'
- // Once the mentioned forms no longer call $this->get() all this 'setting'
- // is obsolete.
- $storeParams = [
- 'savedMapping' => $this->getSubmittedValue('savedMapping'),
- ];
-
- foreach ($storeParams as $storeName => $value) {
- $this->set($storeName, $value);
- }
- }
-
- /**
- * General function for handling invalid configuration.
- *
- * I was going to statusBounce them all but when I tested I was 'bouncing' to weird places
- * whereas throwing an exception gave no behaviour change. So, I decided to centralise
- * and we can 'flip the switch' later.
- *
- * @param $message
- *
- * @throws \CRM_Core_Exception
- */
- protected function invalidConfig($message) {
- throw new CRM_Core_Exception($message);
+ public function setDefaultValues(): array {
+ return array_merge([
+ 'contactType' => 'Individual',
+ 'disableUSPS' => TRUE,
+ ], parent::setDefaultValues());
}
/**
*/
class CRM_Contact_Import_Form_Preview extends CRM_Import_Form_Preview {
+ /**
+ * Get the name of the type to be stored in civicrm_user_job.type_id.
+ *
+ * @return string
+ */
+ public function getUserJobType(): string {
+ return 'contact_import';
+ }
+
/**
* Build the form object.
*/
$this->setTitle($userJob['job_type:label']);
$onDuplicate = $userJob['metadata']['submitted_values']['onDuplicate'];
$this->assign('dupeError', FALSE);
+ $importBaseURL = $this->getUserJobInfo()['url'] ?? NULL;
+ $this->assign('templateURL', ($importBaseURL && $this->getTemplateID()) ? CRM_Utils_System::url($importBaseURL, ['template_id' => $this->getTemplateID(), 'reset' => 1]) : '');
+ // This can be overridden by Civi-Import so that the Download url
+ // links that go to SearchKit open in a new tab.
+ $this->assign('isOpenResultsInNewTab');
+ $this->assign('allRowsUrl');
+ $this->assign('importedRowsUrl');
if ($onDuplicate === CRM_Import_Parser::DUPLICATE_UPDATE) {
$this->assign('dupeActionString', ts('These records have been updated with the imported data.'));
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
+use Civi\Api4\Mapping;
use Civi\Api4\UserJob;
use Civi\Core\ClassScanner;
+use Civi\Core\Event\PreEvent;
+use Civi\Core\HookInterface;
use Civi\UserJob\UserJobInterface;
/**
* This class contains user jobs functionality.
*/
-class CRM_Core_BAO_UserJob extends CRM_Core_DAO_UserJob implements \Civi\Core\HookInterface {
+class CRM_Core_BAO_UserJob extends CRM_Core_DAO_UserJob implements HookInterface {
/**
* Check on the status of a queue.
}
}
+ /**
+ * Enforce template expectations by unsetting non-template variables.
+ *
+ * Also delete the template if the Mapping is deleted.
+ *
+ * @param \Civi\Core\Event\PreEvent $event
+ *
+ * @noinspection PhpUnused
+ * @throws \CRM_Core_Exception
+ */
+ public static function on_hook_civicrm_pre(PreEvent $event): void {
+ if ($event->entity === 'UserJob' &&
+ (!empty($event->params['is_template'])
+ || ($event->action === 'edit' && self::isTemplate($event->params['id']))
+ )) {
+ $params = &$event->params;
+ if (empty($params['name']) && empty($params['id'])) {
+ throw new CRM_Core_Exception('Name is required for template user job');
+ }
+ if ($params['metadata']['submitted_values']['dataSource'] ?? NULL === 'CRM_Import_DataSource_SQL') {
+ // This contains path information that we are better to ditch at this point.
+ // Ideally we wouldn't save this in submitted values - but just use it.
+ unset($params['metadata']['submitted_values']['uploadFile']);
+ }
+ // This contains information about the import-specific data table.
+ unset($params['metadata']['DataSource']['table_name']);
+ // Do not keep values about updating the Mapping/UserJob template.
+ unset($params['metadata']['MapField']['saveMapping'], $params['metadata']['MapField']['updateMapping']);
+ }
+
+ // If the related mapping is deleted then delete the UserJob template
+ // This almost never happens in practice...
+ if ($event->entity === 'Mapping' && $event->action === 'delete') {
+ $mappingName = Mapping::get(FALSE)->addWhere('id', '=', $event->id)->addSelect('name')->execute()->first()['name'];
+ UserJob::delete(FALSE)->addWhere('name', '=', 'import_' . $mappingName)->execute();
+ }
+ }
+
+ /**
+ * Is this id a Template.
+ *
+ * @param int $id
+ *
+ * @return bool
+ * @throws \CRM_Core_Exception
+ */
+ private static function isTemplate(int $id) : bool {
+ return (bool) UserJob::get(FALSE)->addWhere('id', '=', $id)
+ ->addWhere('is_template', '=', 1)
+ ->selectRowCount()->execute()->rowCount;
+ }
+
private static function findUserJobId(string $queueName): ?int {
if (CRM_Core_Config::isUpgradeMode()) {
return NULL;
* @throws \CRM_Core_Exception
*/
public function setDefaultValues(): array {
- parent::setDefaultValues();
- $defaults['contactType'] = 'Individual';
- // Perhaps never used, but permits url passing of the group.
- $defaults['multipleCustomData'] = CRM_Utils_Request::retrieve('id', 'Positive', $this);
-
- $loadedMapping = $this->get('loadedMapping');
- if ($loadedMapping) {
- $defaults['savedMapping'] = $loadedMapping;
- }
-
- return $defaults;
+ return array_merge(parent::setDefaultValues(), [
+ 'contactType' => 'Individual',
+ // Perhaps never used, but permits url passing of the group.
+ 'multipleCustomData' => CRM_Utils_Request::retrieve('id', 'Positive', $this),
+ ]);
}
/**
*/
protected function getTableName(): ?string {
// The old name is still stored...
- $tableName = $this->getDataSourceMetadata()['table_name'];
+ $tableName = $this->getDataSourceMetadata()['table_name'] ?? NULL;
if (!$tableName) {
return NULL;
}
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
+use Civi\Api4\Mapping;
use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\UserJob;
/**
* Base class for upload-only import forms (all but Contact import).
*/
abstract class CRM_Import_Form_DataSource extends CRM_Import_Forms {
+ /**
+ * Values loaded from a saved UserJob template.
+ *
+ * Within Civi-Import it is possible to save a UserJob with is_template = 1.
+ *
+ * @var array
+ */
+ protected $templateValues = [];
+
/**
* Set variables up before form is built.
*/
return (string) CoreUtil::getInfoItem($this->getBaseEntity(), 'title');
}
+ /**
+ * Get the mapping ID that is being loaded.
+ *
+ * @return int|null
+ * @throws \CRM_Core_Exception
+ */
+ public function getSavedMappingID(): ?int {
+ return $this->getSubmittedValue('savedMapping') ?: NULL;
+ }
+
/**
* Get the import entity plural (translated).
*
CRM.$('#data-source-form-block').toggle()",
]);
}
+ if ($this->getTemplateID()) {
+ $this->setTemplateDefaults();
+ }
$this->add('select', 'dataSource', ts('Data Source'), $this->getDataSources(), TRUE,
['onchange' => 'buildDataSourceFormBlock(this.value);']
);
$mappingArray = CRM_Core_BAO_Mapping::getCreateMappingValues('Import ' . $this->getBaseEntity());
- $this->add('select', 'savedMapping', ts('Saved Field Mapping'), ['' => ts('- select -')] + $mappingArray);
- if ($loadedMapping = $this->get('loadedMapping')) {
- $this->setDefaults(['savedMapping' => $loadedMapping]);
+ $savedMappingElement = $this->add('select', 'savedMapping', ts('Saved Field Mapping'), ['' => ts('- select -')] + $mappingArray);
+ if ($this->getTemplateID()) {
+ $savedMappingElement->freeze();
}
//build date formats
return array_merge($this->dataSourceDefaults, [
'dataSource' => $this->getDefaultDataSource(),
'onDuplicate' => CRM_Import_Parser::DUPLICATE_SKIP,
- ]);
-
+ ], $this->templateValues);
}
/**
*
* @param array $names
*/
- protected function storeFormValues($names) {
+ protected function storeFormValues(array $names): void {
foreach ($names as $name) {
$this->set($name, $this->controller->exportValue($this->_name, $name));
}
}
/**
- * Process the datasource submission - setting up the job and data source.
+ * Load default values from the relevant template if one is passed in via the url.
*
- * @throws \CRM_Core_Exception
+ * We need to create and UserJob at this point as the relevant values
+ * go beyond the first DataSource screen.
+ *
+ * @return array
+ * @noinspection PhpUnhandledExceptionInspection
+ * @noinspection PhpDocMissingThrowsInspection
+ */
+ public function setTemplateDefaults(): array {
+ $templateID = $this->getTemplateID();
+ if ($templateID && !$this->getUserJobID()) {
+ $userJob = UserJob::get(FALSE)->addWhere('id', '=', $templateID)->execute()->first();
+ $userJobName = $userJob['name'];
+ // Strip off import_ prefix from UserJob.name
+ $mappingName = substr($userJobName, 7);
+ $mappingID = Mapping::get(FALSE)->addWhere('name', '=', $mappingName)->addSelect('id')->execute()->first()['id'];
+ // Unset fields that should not be copied over.
+ unset($userJob['id'], $userJob['name'], $userJob['created_id'], $userJob['created_date'], $userJob['expires_date'], $userJob['is_template'], $userJob['queue_id'], $userJob['start_date'], $userJob['end_date']);
+ $userJob['metadata']['template_id'] = $templateID;
+ $userJobID = UserJob::create(FALSE)->setValues($userJob)->execute()->first()['id'];
+ $this->set('user_job_id', $userJobID);
+ $userJob['metadata']['submitted_values']['savedMapping'] = $mappingID;
+ $this->templateValues = $userJob['metadata']['submitted_values'];
+ }
+ return [];
+ }
+
+ /**
+ * Process the datasource submission - setting up the job and data source.
*/
protected function processDatasource(): void {
try {
* @noinspection PhpUnhandledExceptionInspection
*/
public function postProcess() {
- $this->updateUserJobMetadata('submitted_values', $this->getSubmittedValues());
+ // This savedMappingID is the one selected on DataSource. It will be overwritten in saveMapping if any
+ // action was taken on it.
+ $this->savedMappingID = $this->getSubmittedValue('savedMapping') ?: NULL;
$this->saveMapping();
+ $this->updateUserJobMetadata('submitted_values', $this->getSubmittedValues());
$parser = $this->getParser();
$parser->init();
$parser->validate();
// @todo we should stop doing this - the passed in value should be fine, confirmed OK in contact import.
$savedMapping = $this->get('savedMapping');
$mappingName = (string) civicrm_api3('Mapping', 'getvalue', ['id' => $savedMappingID, 'return' => 'name']);
- // @todo - this should go too - used when going back to the DataSource form but it should
- // access the job.
- $this->set('loadedMapping', $savedMapping);
$this->add('hidden', 'mappingId', $savedMapping);
$this->addElement('checkbox', 'updateMapping', ts('Update this field mapping'), NULL);
protected function saveMapping(): void {
//Updating Mapping Records
if ($this->getSubmittedValue('updateMapping')) {
+ $savedMappingID = (int) $this->getSubmittedValue('mappingId');
foreach (array_keys($this->getColumnHeaders()) as $i) {
- $this->saveMappingField((int) $this->getSubmittedValue('mappingId'), $i, TRUE);
+ $this->saveMappingField($savedMappingID, $i, TRUE);
}
- $this->updateUserJobMetadata('mapping', ['id' => (int) $this->getSubmittedValue('mappingId')]);
+ $this->setSavedMappingID($savedMappingID);
+ $this->updateUserJobMetadata('Template', ['mapping_id' => (int) $this->getSubmittedValue('mappingId')]);
}
//Saving Mapping Details and Records
if ($this->getSubmittedValue('saveMapping')) {
'description' => $this->getSubmittedValue('saveMappingDesc'),
'mapping_type_id:name' => $this->getMappingTypeName(),
])->execute()->first()['id'];
- $this->updateUserJobMetadata('MapField', ['mapping_id' => $savedMappingID]);
+ $this->setSavedMappingID($savedMappingID);
+ $this->updateUserJobMetadata('Template', ['mapping_id' => $savedMappingID]);
foreach (array_keys($this->getColumnHeaders()) as $i) {
$this->saveMappingField($savedMappingID, $i, FALSE);
}
$this->add('text', 'saveMappingDesc', ts('Description'));
}
else {
- // @todo we should stop doing this - the passed in value should be fine, confirmed OK in contact import.
- $savedMapping = $this->get('savedMapping');
- $mappingName = (string) civicrm_api3('Mapping', 'getvalue', ['id' => $savedMappingID, 'return' => 'name']);
- // @todo - this should go too - used when going back to the DataSource form but it should
- // access the job.
- $this->set('loadedMapping', $savedMapping);
- $this->add('hidden', 'mappingId', $savedMapping);
+ $this->add('hidden', 'mappingId', $savedMappingID);
$this->addElement('checkbox', 'updateMapping', ts('Update this field mapping'), NULL);
$saveDetailsName = ts('Save as a new field mapping');
$this->add('text', 'saveMappingName', ts('Name'));
$this->add('text', 'saveMappingDesc', ts('Description'));
}
- $this->assign('savedMappingName', $mappingName ?? NULL);
+ $this->assign('savedMappingName', $this->getMappingName());
$this->addElement('checkbox', 'saveMapping', $saveDetailsName, NULL);
$this->addFormRule(['CRM_Import_Form_MapField', 'mappingRule']);
}
return $this->defaultFromHeader($columnHeader, $headerPatterns);
}
- /**
- * @return int
- */
- protected function getSavedMappingID(): int {
- $savedMappingID = (int) ($this->getUserJob()['metadata']['MapField']['mapping_id'] ?? $this->getSubmittedValue('savedMapping'));
- return $savedMappingID;
- }
-
}
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
+use Civi\Api4\Mapping;
use Civi\Api4\UserJob;
use League\Csv\Writer;
*/
class CRM_Import_Forms extends CRM_Core_Form {
+
+ /**
+ * @var int
+ */
+ protected $templateID;
+
/**
* User job id.
*
*/
protected $userJobID;
+ /**
+ * Name of the import mapping (civicrm_mapping).
+ *
+ * @var string
+ */
+ protected $mappingName;
+
+ /**
+ * The id of the saved mapping being updated.
+ *
+ * Note this may not be the same as the saved mapping being used to
+ * load data. Use the `getSavedMappingID` function to access & any
+ * extra logic can be added in there.
+ *
+ * @var int
+ */
+ protected $savedMappingID;
+
+ /**
+ * @param int $savedMappingID
+ *
+ * @return CRM_Import_Forms
+ */
+ public function setSavedMappingID(int $savedMappingID): CRM_Import_Forms {
+ $this->savedMappingID = $savedMappingID;
+ return $this;
+ }
+
+ /**
+ * Get the name of the type to be stored in civicrm_user_job.type_id.
+ *
+ * This should be overridden.
+ *
+ * @return string
+ */
+ public function getUserJobType(): string {
+ CRM_Core_Error::deprecatedWarning('this function should be overridden');
+ return '';
+ }
+
/**
* @return int|null
*/
* @param string $fieldName
*
* @return mixed|null
+ * @throws \CRM_Core_Exception
*/
public function getSubmittedValue(string $fieldName) {
if ($fieldName === 'dataSource') {
}
+ /**
+ * Get the template ID from the url, if available.
+ *
+ * Otherwise there are other possibilities...
+ * - it could already be saved to our UserJob.
+ * - on the DataSource form we could determine if from the savedMapping field
+ * (which will hold an ID that can be used to load it). We want to check this is
+ * coming from the POST (ie fresh)
+ * - on the MapField form it could be derived from the new mapping created from
+ * saveMapping + saveMappingName.
+ *
+ * @return int|null
+ * @noinspection PhpUnhandledExceptionInspection
+ * @noinspection PhpDocMissingThrowsInspection
+ */
+ public function getTemplateID(): ?int {
+ if ($this->templateID === NULL) {
+ $this->templateID = CRM_Utils_Request::retrieve('template_id', 'Int', $this);
+ if ($this->templateID && $this->getTemplateJob()) {
+ return $this->templateID;
+ }
+ if ($this->getUserJobID()) {
+ $this->templateID = $this->getUserJob()['metadata']['template_id'] ?? NULL;
+ }
+ elseif (!empty($this->getSubmittedValue('savedMapping'))) {
+ if (!$this->getTemplateJob()) {
+ $this->createTemplateJob();
+ }
+ }
+ }
+ return $this->templateID ?? NULL;
+ }
+
+ /**
+ * @return string
+ *
+ * @throws \CRM_Core_Exception
+ */
+ protected function getMappingName(): string {
+ if ($this->mappingName === NULL) {
+ $savedMappingID = $this->getSavedMappingID();
+ if ($savedMappingID) {
+ $this->mappingName = Mapping::get(FALSE)
+ ->addWhere('id', '=', $savedMappingID)
+ ->execute()
+ ->first()['name'];
+ }
+ }
+ return $this->mappingName ?? '';
+ }
+
/**
* Get the available datasource.
*
// We give the datasource a chance to clean up any tables it might have
// created. If we are still using the same type of datasource (e.g still
// an sql query
- $oldDataSource = $this->getUserJobSubmittedValues()['dataSource'];
- $oldDataSourceObject = new $oldDataSource($this->getUserJobID());
- $newParams = $this->getSubmittedValue('dataSource') === $oldDataSource ? $this->getSubmittedValues() : [];
- $oldDataSourceObject->purge($newParams);
+ $oldDataSource = $this->getUserJobSubmittedValues()['dataSource'] ?? NULL;
+ if ($oldDataSource) {
+ // Absence of an old data source likely means a template has been used (hence
+ // the user job exists) - but templates don't have data sources - so nothing to flush.
+ $oldDataSourceObject = new $oldDataSource($this->getUserJobID());
+ $newParams = $this->getSubmittedValue('dataSource') === $oldDataSource ? $this->getSubmittedValues() : [];
+ $oldDataSourceObject->purge($newParams);
+ }
$this->updateUserJobMetadata('DataSource', []);
}
* all forms.
*
* @return string[]
+ * @throws \CRM_Core_Exception
*/
protected function getSubmittableFields(): array {
$dataSourceFields = array_fill_keys($this->getDataSourceFields(), 'DataSource');
'expires_date' => '+ 1 week',
'metadata' => [
'submitted_values' => $this->getSubmittedValues(),
+ 'template_id' => $this->getTemplateID(),
+ 'Template' => ['mapping_id' => $this->getSavedMappingID()],
],
])
->execute()
return $id;
}
+ protected function createTemplateJob(): void {
+ if (!$this->getUserJobType()) {
+ // This could be hit in extensions while they transition.
+ CRM_Core_Error::deprecatedWarning('Classes should implement getUserJobType');
+ return;
+ }
+ $this->templateID = UserJob::create(FALSE)->setValues([
+ 'is_template' => 1,
+ 'created_id' => CRM_Core_Session::getLoggedInContactID(),
+ 'job_type' => $this->getUserJobType(),
+ 'status_id:name' => 'draft',
+ 'name' => 'import_' . $this->getMappingName(),
+ 'metadata' => ['submitted_values' => $this->getSubmittedValues()],
+ ])->execute()->first()['id'];
+ }
+
/**
* @param string $key
* @param array $data
$this->getUserJob()['metadata'],
[$key => $data]
);
+ $this->getUserJob()['metadata'] = $metaData;
+ if ($this->isUpdateTemplateJob()) {
+ $this->updateTemplateUserJob($metaData);
+ }
+ // We likely don't need the empty check. A precaution against nulling it out by accident.
+ if (empty($metaData['template_id'])) {
+ $metaData['template_id'] = $this->templateID;
+ }
UserJob::update(FALSE)
->addWhere('id', '=', $this->getUserJobID())
->setValues(['metadata' => $metaData])
$this->userJob['metadata'] = $metaData;
}
+ /**
+ * Is the user wanting to update the template / mapping.
+ *
+ * @return bool
+ *
+ * @throws \CRM_Core_Exception
+ */
+ protected function isUpdateTemplateJob(): bool {
+ return $this->getSubmittedValue('updateMapping') || $this->getSubmittedValue('saveMapping');
+ }
+
/**
* Get column headers for the datasource or empty array if none apply.
*
return $summary;
}
+ /**
+ * Get information about the user job parser.
+ *
+ * This is as per `CRM_Core_BAO_UserJob::getTypes()`
+ *
+ * @return array
+ */
+ protected function getUserJobInfo(): array {
+ $importInformation = $this->getParser()->getUserJobInfo();
+ return reset($importInformation);
+ }
+
/**
* Get the fields available for import selection.
*
* Get an instance of the parser class.
*
* @return \CRM_Contact_Import_Parser_Contact|\CRM_Contribute_Import_Parser_Contribution
+ * @throws \CRM_Core_Exception
*/
protected function getParser() {
foreach (CRM_Core_BAO_UserJob::getTypes() as $jobType) {
]);
}
+ /**
+ * Get the UserJob Template, if it exists.
+ *
+ * @return array|null
+ *
+ * @throws \CRM_Core_Exception
+ */
+ protected function getTemplateJob(): ?array {
+ $mappingName = $this->getMappingName();
+ if (!$mappingName) {
+ return NULL;
+ }
+ $templateJob = UserJob::get(FALSE)
+ ->addWhere('name', '=', 'import_' . $mappingName)
+ ->addWhere('is_template', '=', TRUE)
+ ->execute()->first();
+ $this->templateID = $templateJob['id'];
+ return $templateJob ?? NULL;
+ }
+
+ /**
+ * @param array $metaData
+ *
+ * @throws \CRM_Core_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ protected function updateTemplateUserJob(array $metaData): void {
+ if ($this->getTemplateID()) {
+ UserJob::update(FALSE)
+ ->addWhere('id', '=', $this->getTemplateID())
+ ->setValues(['metadata' => $metaData, 'is_template' => TRUE])
+ ->execute();
+ }
+ elseif ($this->getMappingName()) {
+ $this->createTemplateJob();
+ }
+ }
+
+ /**
+ * Get the saved mapping ID being updated.
+ *
+ * @return int|null
+ */
+ public function getSavedMappingID(): ?int {
+ if (!$this->savedMappingID) {
+ if (!empty($this->getUserJob()['metadata']['Template']['mapping_id'])) {
+ $this->savedMappingID = $this->getUserJob()['metadata']['Template']['mapping_id'];
+ }
+ }
+ return $this->savedMappingID;
+ }
+
}
*/
class CRM_Report_Form_Contact_Summary extends CRM_Report_Form {
- public $_summary = NULL;
+ public $_summary;
protected $_emailField = FALSE;
* Load the angular app for our form.
*
* @param string $formName
- * @param \CRM_Core_Form|CRM_Contribute_Import_Form_MapField $form
+ * @param CRM_Contribute_Import_Form_MapField $form
*
* @throws \CRM_Core_Exception
*/
// Add import-ui app
Civi::service('angularjs.loader')->addModules('crmCiviimport');
$form->assignCiviimportVariables();
- $savedMappingID = (int) $form->getSubmittedValue('savedMapping');
+ $savedMappingID = (int) $form->getSavedMappingID();
$savedMapping = [];
if ($savedMappingID) {
$savedMapping = Mapping::get()->addWhere('id', '=', $savedMappingID)->addSelect('id', 'name', 'description')->execute()->first();
$form->assign('isOpenResultsInNewTab', TRUE);
$form->assign('downloadErrorRecordsUrl', CRM_Utils_System::url('civicrm/search', '', TRUE, '/display/Import_' . $form->getUserJobID() . '/Import_' . $form->getUserJobID() . '?_status=ERROR', FALSE));
$form->assign('allRowsUrl', CRM_Utils_System::url('civicrm/search', '', TRUE, '/display/Import_' . $form->getUserJobID() . '/Import_' . $form->getUserJobID(), FALSE));
+ $form->assign('importedRowsUrl', CRM_Utils_System::url('civicrm/search', '', TRUE, '/display/Import_' . $form->getUserJobID() . '/Import_' . $form->getUserJobID() . '?_status=IMPORTED', FALSE));
}
}
<strong>{ts}Import has completed successfully.{/ts}</strong>
{/if}
</p>
+ {if $templateURL}
+ <p>
+ {ts 1=$templateURL|smarty:nodefaults}You can re-use this import configuration <a href="%1">here</a>{/ts}</p>
+ {/if}
{if $unMatchCount}
<p class="error">
{* Summary of Import Results (record counts) *}
<table id="summary-counts" class="report">
<tr><td class="label crm-grid-cell">{ts}Total Rows{/ts}</td>
- <td class="data">{$totalRowCount}</td>
+ <td class="data">{if $allRowsUrl} <a href="{$allRowsUrl}" target="_blank" rel="noopener noreferrer">{$totalRowCount}</a>{else}{$totalRowCount}{/if}</td>
<td class="explanation">{ts}Total number of rows in the imported data.{/ts}</td>
</tr>
{if $unprocessedRowCount}
<tr>
<td class="label crm-grid-cell">{ts}Total Rows Imported{/ts}</td>
- <td class="data">{$importedRowCount}</td>
+ <td class="data">{if $importedRowsUrl} <a href="{$importedRowsUrl}" target="_blank" rel="noopener noreferrer">{$importedRowCount}</a>{else}{$importedRowCount}{/if}</td>
<td class="explanation">{ts}Total number of primary records created or modified during the import.{/ts}</td>
</tr>
{foreach from=$trackingSummary item="summaryRow"}
<tr class="crm-import-uploadfile-form-block-savedMapping">
<td class="label"><label for="savedMapping">{$form.savedMapping.label}</label></td>
<td>{$form.savedMapping.html}<br />
- <span class="description">{ts}If you want to use a previously saved import field mapping - select it here.{/ts}</span>
+ {if !$form.savedMapping.frozen}<span class="description">{ts}If you want to use a previously saved import field mapping - select it here.{/ts}</span>{/if}
</td>
</tr>
{/if}
* Delete any saved mapping config.
*/
public function tearDown(): void {
- $this->quickCleanup(['civicrm_mapping', 'civicrm_mapping_field'], TRUE);
+ $this->quickCleanup(['civicrm_mapping', 'civicrm_mapping_field', 'civicrm_user_job', 'civicrm_queue'], TRUE);
parent::tearDown();
}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+use Civi\Api4\Mapping;
+use Civi\Api4\UserJob;
+
+/**
+ * Test various forms extending CRM_Import_Forms.
+ *
+ * @package CiviCRM
+ * @group import
+ */
+class CRM_Import_FormsTest extends CiviUnitTestCase {
+
+ public function tearDown(): void {
+ $this->quickCleanup(['civicrm_user_job', 'civicrm_mapping', 'civicrm_mapping_field', 'civicrm_queue']);
+ parent::tearDown();
+ }
+
+ /**
+ * @throws \CRM_Core_Exception
+ */
+ public function testLoadDataSourceSavedTemplate(): void {
+ // First do a basic submission, creating a Mapping and UserJob template in the process.
+ [$templateJob, $mapping] = $this->runImportSavingImportTemplate();
+
+ // Now try this template in in the url to load the defaults for DataSource.
+ $_REQUEST['template_id'] = $templateJob['id'];
+ $form = $this->getFormObject('CRM_Contribute_Import_Form_DataSource');
+ $this->formController = $form->controller;
+ $form->buildForm();
+ $defaults = $this->getFormDefaults($form);
+ // These next 2 fields should be loaded as defaults from the UserJob template.
+ $this->assertEquals('Organization', $defaults['contactType']);
+ $this->assertEquals([$mapping['id']], $defaults['savedMapping']);
+ }
+
+ /**
+ * Test that when we Process the MapField form without updating the saved template it is still retained.
+ *
+ * This is important because if we use the BACK button we still want 'Update Mapping'
+ * to show.
+ */
+ public function testSaveRetainingMappingID(): void {
+ // First do a basic submission, creating a Mapping and UserJob template in the process.
+ [, $mapping] = $this->runImportSavingImportTemplate();
+ $this->formController = NULL;
+
+ $dataSourceForm = $this->processForm('CRM_Contribute_Import_Form_DataSource', [
+ 'contactType' => 'Organization',
+ 'savedMapping' => 1,
+ ]);
+ $userJobID = $dataSourceForm->getUserJobID();
+ $this->processForm('CRM_Contribute_Import_Form_MapField', [
+ 'savedMapping' => $mapping['id'],
+ 'contactType' => 'Organization',
+ 'mapper' => [['id'], ['source']],
+ ]);
+
+ // Now we want to submit this form without updating the mapping used & make sure the mapping_id
+ // is still saved in the metadata.
+ /* @var CRM_Contribute_Import_Form_MapField $mapFieldForm */
+ $mapFieldValues = [
+ 'dataSource' => 'CRM_Import_DataSource_SQL',
+ 'sqlQuery' => 'SELECT id, source FROM civicrm_contact',
+ 'mapper' => [['id'], ['financial_type_id']],
+ ];
+ $mapFieldForm = $this->getFormObject('CRM_Contribute_Import_Form_MapField', $mapFieldValues);
+ $mapFieldForm->buildForm();
+
+ $userJob = UserJob::get()->addWhere('id', '=', $userJobID)->execute()->first();
+ $this->assertEquals($mapping['id'], $userJob['metadata']['Template']['mapping_id']);
+ }
+
+ /**
+ * Get the values specified as defaults for the form.
+ *
+ * I originally wanted to make this a public function on `CRM_Core_Form`
+ * but I think it might need to mature first.
+ */
+ public function getFormDefaults($form): array {
+ $defaults = [];
+ if (!empty($form->_elementIndex)) {
+ foreach ($form->_elementIndex as $elementName => $elementIndex) {
+ $element = $form->_elements[$elementIndex];
+ $defaults[$elementName] = $element->getValue();
+ }
+ }
+ return $defaults;
+ }
+
+ /**
+ * @param string $class
+ * @param array $formValues
+ *
+ * @return \CRM_Core_Form
+ */
+ protected function processForm(string $class, array $formValues = []): CRM_Core_Form {
+ $form = $this->getImportForm($class, $formValues);
+ $form->buildForm();
+ $form->mainProcess();
+ return $form;
+ }
+
+ /**
+ * Get some default values to use when we don't care.
+ *
+ * @return array
+ */
+ protected function getDefaultValues(): array {
+ return [
+ 'contactType' => 'Individual',
+ 'contactSubType' => '',
+ 'dataSource' => 'CRM_Import_DataSource_SQL',
+ 'sqlQuery' => 'SELECT id, source FROM civicrm_contact',
+ 'onDuplicate' => CRM_Import_Parser::DUPLICATE_UPDATE,
+ 'mapper' => [['id'], ['source']],
+ ];
+ }
+
+ /**
+ * @param array $submittedValues
+ */
+ protected function processContributionForms(array $submittedValues): void {
+ try {
+ $this->processForm('CRM_Contribute_Import_Form_DataSource', $submittedValues);
+ $this->processForm('CRM_Contribute_Import_Form_MapField', $submittedValues);
+ $this->processForm('CRM_Contribute_Import_Form_Preview', $submittedValues);
+ }
+ catch (CRM_Core_Exception_PrematureExitException $e) {
+ // We expect this to happen as it re-directs to the queue runner.
+ }
+ }
+
+ /**
+ * @param string $class
+ * @param array $formValues
+ *
+ * @return \CRM_Core_Form
+ */
+ protected function getImportForm(string $class, array $formValues = []): CRM_Core_Form {
+ $formValues = array_merge($this->getDefaultValues(), $formValues);
+ return $this->getFormObject($class, $formValues);
+ }
+
+ /**
+ * @return array
+ */
+ protected function runImportSavingImportTemplate(): array {
+ $this->processContributionForms([
+ 'saveMapping' => 1,
+ 'saveMappingName' => 'mapping',
+ 'contactType' => 'Organization',
+ ]);
+
+ // Check that a template job and a mapping have been created.
+ $templateJob = UserJob::get()
+ ->addWhere('is_template', '=', 1)
+ ->execute()
+ ->first();;
+ $this->assertNotEmpty($templateJob);
+ $this->assertArrayNotHasKey('table_name', $templateJob['metadata']['DataSource']);
+ $mapping = Mapping::get()
+ ->addWhere('name', '=', substr($templateJob['name'], 7))
+ ->execute()
+ ->first();
+ $this->assertNotEmpty($mapping);
+ // Reset the formController so this doesn't leak into further tests.
+ $this->formController = NULL;
+ return [$templateJob, $mapping];
+ }
+
+}
$this->unsetExtensionSystem();
$this->assertEquals([], CRM_Core_DAO::$_nullArray);
$this->assertEquals(NULL, CRM_Core_DAO::$_nullObject);
- // Ensure the destruct runs by unsetting it. Also, unsetting
- // classes frees memory as they are not otherwise unset until the
- // very end.
+ // Setting large properties to NULL here ensures memory is released as each
+ // test class is held in memory until the very end.
+ $this->formController = NULL;
+ // Ensure the destruct runs by unsetting the Mutt.
unset($this->mut);
parent::tearDown();
}
case 'CRM_Contribute_Import_Form_DataSource':
case 'CRM_Contribute_Import_Form_MapField':
case 'CRM_Contribute_Import_Form_Preview':
- $form->controller = new CRM_Contribute_Import_Controller();
- $form->controller->setStateMachine(new CRM_Core_StateMachine($form->controller));
+ if ($this->formController) {
+ // Add to the existing form controller.
+ $form->controller = $this->formController;
+ }
+ else {
+ $form->controller = new CRM_Contribute_Import_Controller();
+ $form->controller->setStateMachine(new CRM_Core_StateMachine($form->controller));
+ $this->formController = $form->controller;
+ }
// The submitted values should be set on one or the other of the forms in the flow.
// For test simplicity we set on all rather than figuring out which ones go where....
$_SESSION['_' . $form->controller->_name . '_container']['values']['DataSource'] = $formValues;