Merge pull request #23419 from chrisgaraffa/contactheader-regions
[civicrm-core.git] / CRM / Import / Form / MapField.php
CommitLineData
b26295b8
CW
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
b26295b8 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
b26295b8 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
b26295b8
CW
11
12/**
b26295b8 13 * @package CRM
ca5cec67 14 * @copyright CiviCRM LLC https://civicrm.org/licensing
b26295b8
CW
15 */
16
b4167b7c 17use Civi\Api4\Mapping;
99308da4
EM
18use Civi\Api4\MappingField;
19
b26295b8 20/**
2b4bc760 21 * This class gets the name of the file to upload.
22 *
b26295b8
CW
23 * TODO: CRM-11254 - There's still a lot of duplicate code in the 5 child classes that should be moved here
24 */
5e8faabc 25abstract class CRM_Import_Form_MapField extends CRM_Import_Forms {
b26295b8
CW
26
27 /**
100fef9d 28 * Cache of preview data values
b26295b8
CW
29 *
30 * @var array
b26295b8
CW
31 */
32 protected $_dataValues;
33
34 /**
100fef9d 35 * Mapper fields
b26295b8
CW
36 *
37 * @var array
b26295b8
CW
38 */
39 protected $_mapperFields;
40
b26295b8 41 /**
100fef9d 42 * Number of columns in import file
b26295b8
CW
43 *
44 * @var int
b26295b8
CW
45 */
46 protected $_columnCount;
47
48 /**
100fef9d 49 * Column headers, if we have them
b26295b8
CW
50 *
51 * @var array
b26295b8
CW
52 */
53 protected $_columnHeaders;
54
55 /**
100fef9d 56 * An array of booleans to keep track of whether a field has been used in
b26295b8
CW
57 * form building already.
58 *
59 * @var array
b26295b8
CW
60 */
61 protected $_fieldUsed;
62
63 /**
2b4bc760 64 * Return a descriptive name for the page, used in wizard header.
b26295b8
CW
65 *
66 * @return string
b26295b8
CW
67 */
68 public function getTitle() {
69 return ts('Match Fields');
70 }
71
8d0967f5
EM
72 /**
73 * Shared preProcess code.
74 */
75 public function preProcess() {
76 $this->assignMapFieldVariables();
77a9ae24
EM
77 $this->_mapperFields = $this->getAvailableFields();
78 asort($this->_mapperFields);
8d0967f5
EM
79 parent::preProcess();
80 }
81
992a3d9e
EM
82 /**
83 * Process the mapped fields and map it into the uploaded file
84 * preview the file and extract some summary statistics
85 *
86 * @return void
87 * @noinspection PhpDocSignatureInspection
88 * @noinspection PhpUnhandledExceptionInspection
89 */
90 public function postProcess() {
91 $this->updateUserJobMetadata('submitted_values', $this->getSubmittedValues());
b4167b7c 92 $this->saveMapping();
992a3d9e
EM
93 $parser = $this->getParser();
94 $parser->init();
95 $parser->validate();
96 }
97
9b324cef
EM
98 /**
99 * Add the form buttons.
100 */
101 protected function addFormButtons(): void {
102 $this->addButtons([
103 [
104 'type' => 'back',
105 'name' => ts('Previous'),
106 ],
107 [
108 'type' => 'next',
109 'name' => ts('Continue'),
110 'spacing' => '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;',
111 'isDefault' => TRUE,
112 ],
113 [
114 'type' => 'cancel',
115 'name' => ts('Cancel'),
116 ],
117 ]);
118 }
119
b26295b8 120 /**
fe482240 121 * Attempt to match header labels with our mapper fields.
b26295b8 122 *
79d7553f 123 * @param string $header
124 * @param array $patterns
b26295b8
CW
125 *
126 * @return string
b26295b8
CW
127 */
128 public function defaultFromHeader($header, &$patterns) {
129 foreach ($patterns as $key => $re) {
130 // Skip empty key/patterns
131 if (!$key || !$re || strlen("$re") < 5) {
132 continue;
133 }
134
135 // Scan through the headerPatterns defined in the schema for a match
136 if (preg_match($re, $header)) {
137 $this->_fieldUsed[$key] = TRUE;
138 return $key;
139 }
140 }
141 return '';
142 }
143
144 /**
fe482240 145 * Guess at the field names given the data and patterns from the schema.
b26295b8 146 *
79d7553f 147 * @param array $patterns
148 * @param string $index
b26295b8
CW
149 *
150 * @return string
b26295b8 151 */
56dd62a0 152 public function defaultFromData($patterns, $index) {
353ffa53 153 $best = '';
b26295b8 154 $bestHits = 0;
353ffa53 155 $n = count($this->_dataValues);
b26295b8
CW
156
157 foreach ($patterns as $key => $re) {
158 // Skip empty key/patterns
159 if (!$key || !$re || strlen("$re") < 5) {
160 continue;
161 }
162
163 /* Take a vote over the preview data set */
164 $hits = 0;
165 for ($i = 0; $i < $n; $i++) {
166 if (isset($this->_dataValues[$i][$index])) {
167 if (preg_match($re, $this->_dataValues[$i][$index])) {
168 $hits++;
169 }
170 }
171 }
172 if ($hits > $bestHits) {
173 $bestHits = $hits;
174 $best = $key;
175 }
176 }
177
178 if ($best != '') {
179 $this->_fieldUsed[$best] = TRUE;
180 }
181 return $best;
182 }
183
ad05d047 184 /**
185 * Add the saved mapping fields to the form.
186 *
187 * @param int|null $savedMappingID
188 *
189 * @throws \CiviCRM_API3_Exception
190 */
191 protected function buildSavedMappingFields($savedMappingID) {
192 //to save the current mappings
193 if (!$savedMappingID) {
194 $saveDetailsName = ts('Save this field mapping');
195 $this->applyFilter('saveMappingName', 'trim');
196 $this->add('text', 'saveMappingName', ts('Name'));
197 $this->add('text', 'saveMappingDesc', ts('Description'));
198 }
199 else {
200 $savedMapping = $this->get('savedMapping');
201
202 $mappingName = (string) civicrm_api3('Mapping', 'getvalue', ['id' => $savedMappingID, 'return' => 'name']);
203 $this->set('loadedMapping', $savedMapping);
ad05d047 204 $this->add('hidden', 'mappingId', $savedMappingID);
205
206 $this->addElement('checkbox', 'updateMapping', ts('Update this field mapping'), NULL);
207 $saveDetailsName = ts('Save as a new field mapping');
208 $this->add('text', 'saveMappingName', ts('Name'));
209 $this->add('text', 'saveMappingDesc', ts('Description'));
210 }
262b7f26 211 $this->assign('savedMappingName', $mappingName ?? NULL);
ad05d047 212 $this->addElement('checkbox', 'saveMapping', $saveDetailsName, NULL, ['onclick' => "showSaveDetails(this)"]);
213 }
214
02a237ce 215 /**
216 * Validate that sufficient fields have been supplied to match to a contact.
217 *
218 * @param string $contactType
219 * @param array $importKeys
220 *
221 * @return string
222 * Message if insufficient fields are present. Empty string otherwise.
223 */
224 protected static function validateRequiredContactMatchFields(string $contactType, array $importKeys): string {
61194d45 225 [$ruleFields, $threshold] = CRM_Dedupe_BAO_DedupeRuleGroup::dedupeRuleFieldsWeight([
02a237ce 226 'used' => 'Unsupervised',
227 'contact_type' => $contactType,
228 ]);
229 $weightSum = 0;
230 foreach ($importKeys as $key => $val) {
231 if (array_key_exists($val, $ruleFields)) {
232 $weightSum += $ruleFields[$val];
233 }
234 }
235 $fieldMessage = '';
236 foreach ($ruleFields as $field => $weight) {
237 $fieldMessage .= ' ' . $field . '(weight ' . $weight . ')';
238 }
239 if ($weightSum < $threshold) {
240 return $fieldMessage . ' ' . ts('(Sum of all weights should be greater than or equal to threshold: %1).', array(
241 1 => $threshold,
242 ));
243 }
244 return '';
245 }
246
73edfc10
EM
247 /**
248 * Get the field mapped to the savable format.
249 *
250 * @param array $fieldMapping
251 * @param int $mappingID
252 * @param int $columnNumber
253 *
254 * @return array
255 * @throws \CRM_Core_Exception
256 */
257 protected function getMappedField(array $fieldMapping, int $mappingID, int $columnNumber): array {
258 return $this->getParser()->getMappingFieldFromMapperInput($fieldMapping, $mappingID, $columnNumber);
259 }
260
261 /**
262 * Save the mapping field.
263 *
264 * @param int $mappingID
265 * @param int $columnNumber
266 * @param bool $isUpdate
267 *
268 * @throws \API_Exception
269 * @throws \CRM_Core_Exception
270 */
271 protected function saveMappingField(int $mappingID, int $columnNumber, bool $isUpdate = FALSE): void {
272 $fieldMapping = (array) $this->getSubmittedValue('mapper')[$columnNumber];
273 $mappedField = $this->getMappedField($fieldMapping, $mappingID, $columnNumber);
992a3d9e
EM
274 if (empty($mappedField['name'])) {
275 $mappedField['name'] = 'do_not_import';
276 }
99308da4
EM
277 $existing = MappingField::get(FALSE)
278 ->addWhere('column_number', '=', $columnNumber)
279 ->addWhere('mapping_id', '=', $mappingID)->execute()->first();
280 if (empty($existing['id'])) {
281 MappingField::create(FALSE)
282 ->setValues($mappedField)->execute();
73edfc10
EM
283 }
284 else {
99308da4
EM
285 MappingField::update(FALSE)
286 ->setValues($mappedField)
287 ->addWhere('id', '=', $existing['id'])
288 ->execute();
73edfc10
EM
289 }
290 }
291
77a9ae24
EM
292 /**
293 * Save the Field Mapping.
294 *
77a9ae24
EM
295 * @throws \API_Exception
296 * @throws \CRM_Core_Exception
297 */
b4167b7c 298 protected function saveMapping(): void {
77a9ae24
EM
299 //Updating Mapping Records
300 if ($this->getSubmittedValue('updateMapping')) {
301 foreach (array_keys($this->getColumnHeaders()) as $i) {
302 $this->saveMappingField($this->getSubmittedValue('mappingId'), $i, TRUE);
303 }
304 }
305 //Saving Mapping Details and Records
306 if ($this->getSubmittedValue('saveMapping')) {
b4167b7c 307 $savedMappingID = Mapping::create(FALSE)->setValues([
77a9ae24
EM
308 'name' => $this->getSubmittedValue('saveMappingName'),
309 'description' => $this->getSubmittedValue('saveMappingDesc'),
b4167b7c
EM
310 'mapping_type_id:name' => $this->getMappingTypeName(),
311 ])->execute()->first()['id'];
77a9ae24
EM
312
313 foreach (array_keys($this->getColumnHeaders()) as $i) {
b4167b7c 314 $this->saveMappingField($savedMappingID, $i, FALSE);
77a9ae24 315 }
b4167b7c 316 $this->set('savedMapping', $savedMappingID);
77a9ae24
EM
317 }
318 }
319
7eebbdaa
EM
320 /**
321 * @throws \API_Exception
322 * @throws \CRM_Core_Exception
323 * @throws \Civi\API\Exception\UnauthorizedException
324 */
325 protected function getFieldMappings(): array {
326 $savedMappingID = $this->getSubmittedValue('savedMapping');
327 if ($savedMappingID) {
328 $fieldMappings = MappingField::get(FALSE)
329 ->addWhere('mapping_id', '=', $savedMappingID)
330 ->execute()
331 ->indexBy('column_number');
332
333 if ((count($this->getColumnHeaders()) !== count($fieldMappings))) {
334 CRM_Core_Session::singleton()->setStatus(ts('The data columns in this import file appear to be different from the saved mapping. Please verify that you have selected the correct saved mapping before continuing.'));
335 }
336 return (array) $fieldMappings;
337 }
338 return [];
339 }
340
739acba7
EM
341 /**
342 * Add the mapper hierarchical select field to the form.
343 *
344 * @return array
345 */
346 protected function addMapper(): array {
347 $defaults = [];
348 $mapperKeys = array_keys($this->_mapperFields);
349 $hasHeaders = $this->getSubmittedValue('skipColumnHeader');
350 $headerPatterns = $this->getHeaderPatterns();
351 $dataPatterns = $this->getDataPatterns();
352 $fieldMappings = $this->getFieldMappings();
353 /* Initialize all field usages to false */
354
355 foreach ($mapperKeys as $key) {
356 $this->_fieldUsed[$key] = FALSE;
357 }
358 $sel1 = $this->_mapperFields;
359
360 $js = "<script type='text/javascript'>\n";
361 $formName = 'document.forms.' . $this->_name;
362
363 foreach ($this->getColumnHeaders() as $i => $columnHeader) {
364 $sel = &$this->addElement('hierselect', "mapper[$i]", ts('Mapper for Field %1', [1 => $i]), NULL);
365 $jsSet = FALSE;
366 if ($this->getSubmittedValue('savedMapping')) {
367 $fieldMapping = $fieldMappings[$i] ?? NULL;
368 if (isset($fieldMappings[$i])) {
369 if ($fieldMapping['name'] !== ts('do_not_import')) {
370 $js .= "{$formName}['mapper[$i][3]'].style.display = 'none';\n";
371 $defaults["mapper[$i]"] = [$fieldMapping['name']];
372 $jsSet = TRUE;
373 }
374 else {
375 $defaults["mapper[$i]"] = [];
376 }
377 if (!$jsSet) {
378 for ($k = 1; $k < 4; $k++) {
379 $js .= "{$formName}['mapper[$i][$k]'].style.display = 'none';\n";
380 }
381 }
382 }
383 else {
384 // this load section to help mapping if we ran out of saved columns when doing Load Mapping
385 $js .= "swapOptions($formName, 'mapper[$i]', 0, 3, 'hs_mapper_" . $i . "_');\n";
386
387 if ($hasHeaders) {
388 $defaults["mapper[$i]"] = [$this->defaultFromHeader($columnHeader, $headerPatterns)];
389 }
390 else {
391 $defaults["mapper[$i]"] = [$this->defaultFromData($dataPatterns, $i)];
392 }
393 }
394 //end of load mapping
395 }
396 else {
397 $js .= "swapOptions($formName, 'mapper[$i]', 0, 3, 'hs_mapper_" . $i . "_');\n";
398 if ($hasHeaders) {
399 // Infer the default from the skipped headers if we have them
400 $defaults["mapper[$i]"] = [
401 $this->defaultFromHeader($columnHeader,
402 $headerPatterns
403 ),
404 // $defaultLocationType->id
405 0,
406 ];
407 }
408 else {
409 // Otherwise guess the default from the form of the data
410 $defaults["mapper[$i]"] = [
411 $this->defaultFromData($dataPatterns, $i),
412 // $defaultLocationType->id
413 0,
414 ];
415 }
416 }
417 $sel->setOptions([$sel1]);
418 }
419 $js .= "</script>\n";
420 $this->assign('initHideBoxes', $js);
421 $this->setDefaults($defaults);
422 return [$sel, $headerPatterns];
423 }
424
b26295b8 425}