-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathimport_helper_2.php
2004 lines (1935 loc) · 80.3 KB
/
import_helper_2.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* @file
* Helper class for data imports version 2.
*
* Indicia, the OPAL Online Recording Toolkit.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/gpl.html.
*
* @license http://www.gnu.org/licenses/gpl.html GPL 3.0
* @link https://github.com/indicia-team/warehouse/
*/
/**
* Link in other required php files.
*/
require_once 'lang.php';
require_once 'lang/import_helper_2.php';
require_once 'helper_base.php';
/**
* Static helper class that provides methods for dealing with imports.
*
* Version 2 which uses a temp table on the warehouse as a place to prepare
* the imported data before actually importing.
*/
class import_helper_2 extends helper_base {
/**
* A file import component.
*
* Options include:
* * blockedFields - array of database field names that should not be listed
* as available for mapping to. Defaults to a list of "advanced" fields
* that are not considered suitable for non-expert imports. Field names may
* be fully qualified ('sample.deleted') or just the field name alone, in
* which case it applies to all tables.
* * entity
* * fileSelectFormIntro
* * globalValuesFormIntro
* * mappingsFormIntro
* * lookupMatchingFormIntro
* * preprocessPageIntro
* * summaryPageIntro
* * doImportPageIntro
* * requiredFieldsIntro
* * uploadFileUrl - path to a script that handles the initial upload of a
* file to the interim file location. The script can call
* import_helper_2::uploadFile for a complete implementation.
* * sendFileToWarehouseUrl - path to a script that forwards the import file
* from the interim location to the warehouse. The script can call
* import_helper_2::sendFileToWarehouse for a complete implementation.
* * initServerConfigUrl - path to a script that initialises the JSON config
* file for the import on the warehouse.
* * loadChunkToTempTableUrl - path to a script that triggers the load of the
* next chunk of records into a temp table on the warehouse. The script can
* call import_helper_2::loadChunkToTempTable for a complete implementation.
* * preprocessUrl - path to a script that performs processing
* that can be done after the file is loaded and mappings done.
* * processLookupMatchingUrl - path to a script that performs steps in the
* process of identifying lookup destination fields that need their values
* to be matched to obtain an ID.
* * saveLookupMatchesGroupUrl - path to a script that saves a set of
* matching data value/matched termlist term ID pairs for a lookup custom
* attribute.
* * importChunkUrl - path to a script that imports the next chunk of records
* * getErrorFileUrl - location of the end-point that fetches the errors data
* file.
* * readAuth - read authorisation tokens.
* * writeAuth - write authorisation tokens.
* * fixedValues - array of fixed key/value pairs that apply to all rows. Can
* also be used to filter the available lookups, by specifying a key/value
* pair where the key is of form
* `<table>:fkFilter:<lookupTable>:<lookupTableFieldToFilter>` and the
* provided value will be applied as a filter. For example, to limit the
* available taxon lists when importing into the locations to list ID 1,
* use `occurrence:fkFilter:taxa_taxon_list:taxon_list_id=1`. To filter the
* location types looked up against when importing samples that link to
* locations, specify `sample:fkFilter:location:location_type_id=n` where
* n is the location type to filter to.
* @todo Document how to get the fixed value field names.
* * fixedValueDefaults - default values for fixedValues that present a list
* of options to the user.
* * allowUpdates - set to true to enable updating existing rows based on an
* ID or external key field mapping. Only affects the user's own data.
* * allowDeletes = set to true to enable mapping to a deleted flag for the
* user's own data. Requires the allowUpdates option to be set.
* * allowImportReverse - adds a control to the file upload page which allows
* a previous export to be selected and reversed.
*/
public static function importer($options) {
if (empty($options['entity'])) {
throw new exception('The import_helper_2::importer control needs an entity option.');
}
if (empty($options['readAuth'])) {
throw new exception('The import_helper_2::importer control needs a readAuth option.');
}
if (empty($options['writeAuth'])) {
throw new exception('The import_helper_2::importer control needs a writeAuth option.');
}
if (empty(hostsite_get_user_field('indicia_user_id'))) {
throw new exception('The import_helper_2::importer control requires the Easy Login module with an account that has been linked to the warehouse.');
}
self::add_resource('uploader');
self::add_resource('font_awesome');
self::add_resource('fancybox');
self::getImportHelperOptions($options);
self::$indiciaData['uploadFileUrl'] = $options['uploadFileUrl'];
self::$indiciaData['sendFileToWarehouseUrl'] = $options['sendFileToWarehouseUrl'];
self::$indiciaData['extractFileOnWarehouseUrl'] = $options['extractFileOnWarehouseUrl'];
self::$indiciaData['initServerConfigUrl'] = $options['initServerConfigUrl'];
self::$indiciaData['loadChunkToTempTableUrl'] = $options['loadChunkToTempTableUrl'];
self::$indiciaData['preprocessUrl'] = $options['preprocessUrl'];
self::$indiciaData['processLookupMatchingUrl'] = $options['processLookupMatchingUrl'];
self::$indiciaData['saveLookupMatchesGroupUrl'] = $options['saveLookupMatchesGroupUrl'];
self::$indiciaData['importChunkUrl'] = $options['importChunkUrl'];
self::$indiciaData['getErrorFileUrl'] = $options['getErrorFileUrl'];
self::$indiciaData['write'] = $options['writeAuth'];
self::$indiciaData['advancedFields'] = $options['advancedFields'];
$nextImportStep = empty($_POST['next-import-step']) ? 'fileSelectForm' : $_POST['next-import-step'];
self::$indiciaData['step'] = $nextImportStep;
switch ($nextImportStep) {
case 'fileSelectForm':
$r = self::fileSelectForm($options);
// The reverser currently assumes occurrence entity.
if ($options['entity'] === 'occurrence' &&
(!empty($options['allowImportReverse']) && $options['allowImportReverse'] == TRUE)) {
$r .= self::importToReverseDropDown($options);
}
return $r;
case 'globalValuesForm':
return self::globalValuesForm($options);
case 'mappingsForm':
return self::mappingsForm($options);
case 'lookupMatchingForm':
return self::lookupMatchingForm($options);
case 'preprocessPage':
return self::preprocessPage($options);
case 'summaryPage':
return self::summaryPage($options);
case 'doImportPage':
return self::doImportPage($options);
case 'reversalModeWarningOrSkipToResult':
return self::reversalModeWarningOrSkipToResult($options);
case 'reversalResult':
return self::reversalResult($options);
default:
throw new exception('Invalid next-import-step parameter');
}
}
/**
* Upload interim file AJAX handler.
*
* Provides functionality that can receive an selected file and saves it to
* the interim location on the client website's server. Suitable for calling
* from a web-service endpoint that can be called from JS (see `importer_2`'s
* `ajax_upload_file` method for an example.)
*
* @return string
* The file name of the saved file.
*/
public static function uploadInterimFile() {
if (!isset($_FILES['file']['error']) || is_array($_FILES['file']['error'])) {
throw new RuntimeException('Invalid parameters.');
}
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
throw new RuntimeException('No file sent.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new RuntimeException('Exceeded filesize limit.');
default:
throw new RuntimeException('Unknown errors.');
}
$targetDir = self::getInterimImageFolder('fullpath');
$fileName = sprintf('%s_%s', uniqid(), $_FILES['file']['name']);
$filePath = $targetDir . $fileName;
if (!move_uploaded_file($_FILES['file']['tmp_name'], $filePath)) {
throw new RuntimeException('Failed to move uploaded file.');
}
// All good.
return $fileName;
}
/**
* Sends a file from the interim location to the warehouse.
*
* Files are initially uploaded to the Drupal server's files dir, this
* method sends the file to the warehouse's import folder.
*
* @param string $fileName
* Name of the file to transfer.
* @param array $writeAuth
* Write authorisation tokens.
*/
public static function sendFileToWarehouse($fileName, array $writeAuth) {
return self::send_file_to_warehouse($fileName, TRUE, $writeAuth, 'import_2/upload_file', TRUE);
}
/**
* Where an import file was zipped, requests unzipping at the warehouse end.
*
* @param string $fileName
* Name of the file.
* @param array $writeAuth
* Write authorisation tokens.
*
* @return array
* Web-service response containing a data-file property with the name of
* the extracted file.
*/
public static function extractFileOnWarehouse($fileName, array $writeAuth) {
$serviceUrl = self ::$base_url . 'index.php/services/import_2/extract_file';
$data = $writeAuth + ['uploaded-file' => $fileName, 'persist_auth' => TRUE];
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
if (!$response['result']) {
throw new exception(isset($output['msg']) ? $output['msg'] : $response['output']);
}
return $output;
}
/**
* Sets up the config JSON file on the server.
*
* @param string $fileName
* Name of the file.
* @param int $importTemplateId
* Template ID if one was selected.
* @param array $writeAuth
* Write authorisation tokens.
* @param array $plugins
* List of enabled plugins as keys, with parameter arrays as values.
*
* @return array
* Output of the web service request.
*/
public static function initServerConfig($fileName, $importTemplateId, array $writeAuth, array $plugins = []) {
$serviceUrl = self ::$base_url . 'index.php/services/import_2/init_server_config';
$data = $writeAuth + [
'data-file' => $fileName,
'import_template_id' => $importTemplateId,
'plugins' => json_encode($plugins),
];
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
if (!$response['result']) {
\Drupal::logger('iform')->notice('Error in initServerConfig: ' . var_export($response, TRUE));
throw new exception(isset($output['msg']) ? $output['msg'] : $response['output']);
}
return $output;
}
/**
* Triggers the load of a chunk of records from the file to the temp table.
*
* @param string $fileName
* Name of the file to transfer.
* @param array $writeAuth
* Write authorisation tokens.
*/
public static function loadChunkToTempTable($fileName, array $writeAuth) {
$serviceUrl = self ::$base_url . 'index.php/services/import_2/load_chunk_to_temp_table';
$data = $writeAuth + ['data-file' => $fileName];
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
if (!$response['result']) {
\Drupal::logger('iform')->notice('Error in loadChunkToTempTable: ' . var_export($response, TRUE));
}
return $output;
}
/**
* Actions the next step in the process of linking data values to lookup IDs.
*
* @param string $fileName
* Name of the file to process.
* @param int $index
* Index of the request - start at 0 then increment by one for each request
* to fetch each lookup that needs matching in turn.
* @param array $writeAuth
* Write authorisation tokens.
*/
public static function processLookupMatching($fileName, $index, array $writeAuth) {
$serviceUrl = self ::$base_url . 'index.php/services/import_2/process_lookup_matching';
$data = $writeAuth + [
'data-file' => $fileName,
'index' => $index,
];
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
if (!$response['result']) {
\Drupal::logger('iform')->notice('Error in processLookupMatching: ' . var_export($response, TRUE));
}
return $output;
}
/**
* Saves the manually matched value / term ID pairings for a custom attr.
*
* @param string $fileName
* Name of the file to process.
* @param array $matchesInfo
* Matching data.
* @param array $writeAuth
* Write authorisation tokens.
*/
public static function saveLookupMatchesGroup($fileName, array $matchesInfo, array $writeAuth) {
$serviceUrl = self ::$base_url . 'index.php/services/import_2/save_lookup_matches_group';
$data = $writeAuth + [
'data-file' => $fileName,
'matches-info' => json_encode($matchesInfo),
];
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
if (!$response['result']) {
\Drupal::logger('iform')->notice('Error in saveLookupMatchesGroup: ' . var_export($response, TRUE));
throw new exception(isset($output['msg']) ? $output['msg'] : $response['output']);
}
return $output;
}
/**
* Perform processing that can be done before the actual import.
*
* Includes global validation and linking to existing records.
*
* @param string $fileName
* Name of the file to process.
* @param int $index
* Index of the request - start at 0 then increment by one for each request
* to perform each processing step in turn.
* @param array $writeAuth
* Write authorisation tokens.
*/
public static function preprocess($fileName, $index, array $writeAuth) {
$serviceUrl = self ::$base_url . 'index.php/services/import_2/preprocess';
$data = $writeAuth + [
'data-file' => $fileName,
'index' => $index,
];
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
if (!$response['result']) {
\Drupal::logger('iform')->notice('Error in preprocess: ' . var_export($response, TRUE));
}
return $output;
}
/**
* Import the next chunk of records to the main database.
*
* @param string $fileName
* Name of the file to process.
* @param array $params
* List of options passed to the import chunk AJAX proxy. Includes:
* * description - Description if saving the import metadata for the first
* time.
* * importTemplateTitle - Title if saving the import configuration as a
* template for future use.
* * forceTemplateOverwrite - Set to true if the template title provided
* can be used to overwrite an existing one of the same name for this
* user.
* * precheck - precheck, which retrieves validation errors without
* importing.
* * restart - forces the warehouse to start from the first row in the
* import.
* @param array $writeAuth
* Write authorisation tokens.
*/
public static function importChunk($fileName, array $params, array $writeAuth) {
$params = array_merge([
'description' => NULL,
'importTemplateTitle' => NULL,
'forceTemplateOverwrite' => FALSE,
'precheck' => FALSE,
'restart' => FALSE,
], $params);
$serviceUrl = self ::$base_url . 'index.php/services/import_2/import_chunk';
$data = $writeAuth + [
'data-file' => $fileName,
];
if (!empty(trim($params['description'] ?? ''))) {
$data['save-import-record'] = json_encode([
'description' => trim($params['description']),
]);
}
if (!empty(trim($params['importTemplateTitle'] ?? ''))) {
$data['save-import-template'] = json_encode([
'title' => trim($params['importTemplateTitle']),
'forceTemplateOverwrite' => $params['forceTemplateOverwrite'],
]);
}
if ($params['precheck']) {
$data['precheck'] = 't';
}
if ($params['restart']) {
$data['restart'] = 't';
}
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
if (!$response['result']) {
if (isset($response['status']) && $response['status'] === 409 && $output['msg'] === 'An import template with that title already exists') {
// Duplicate conflict in import template title.
return [
'status' => 'conflict',
'msg' => $output['msg'],
'title' => trim($params['importTemplateTitle'] ?? ''),
];
}
else {
\Drupal::logger('iform')->notice('Error in importChunk: ' . var_export($response, TRUE));
throw new exception($output['msg'] ?? $response['output']);
}
}
return $output;
}
/**
* Fetch the HTML for the file select form.
*
* @param array $options
* Options array for the control.
*
* @return string
* HTML.
*/
private static function fileSelectForm(array $options) {
self::addLanguageStringsToJs('import_helper_2', [
'invalidType' => 'The chosen file was not a type of file that can be imported.',
'removeUploadedFileHint' => 'Remove the uploaded file',
'uploadFailedWithError' => 'The file upload failed. The error message was:<br/>{1}.',
]);
$lang = [
'browseFiles' => lang::get('Browse files'),
'clickToAdd' => lang::get('Click to add files'),
'instructions' => lang::get($options['fileSelectFormIntro']),
'instructionsSelectTemplate' => lang::get('Choose one of the templates you saved previously if repeating a similar import.'),
'next' => lang::get('Next step'),
'selectAFile' => lang::get('Select a CSV or Excel file or drag it over this area. The file can optionally be a zip archive. If importing an Excel file, only the first worksheet will be imported.'),
'uploadFileToImport' => lang::get('Upload a file to import'),
];
$templates = self::loadTemplates($options);
$templatePickerHtml = '';
if (count($templates)) {
$templateOptions = [];
foreach ($templates as $template) {
$templateOptions[$template['id']] = $template['title'];
}
$templatePickerHtml = data_entry_helper::select([
'fieldname' => 'import_template_id',
'label' => lang::get('Template'),
'helpText' => lang::get('If you would like to import similar data to a previous import, choose one of the templates you saved previously to re-use the settings'),
'lookupValues' => $templateOptions,
'blankText' => lang::get('-no template selected-'),
]);
}
$r = <<<HTML
<h3>$lang[uploadFileToImport]</h3>
<p>$lang[instructions]</p>
<form id="file-upload-form" method="POST">
<div class="dm-uploader row">
<div class="col-md-9">
<div role="button" class="btn btn-primary">
<i class="fas fa-file-upload"></i>
$lang[browseFiles]
<input type="file" title="$lang[clickToAdd]">
</div>
<small class="status text-muted">$lang[selectAFile]</small>
</div>
<div class="col-md-3" id="uploaded-files"></div>
</div>
$templatePickerHtml
<progress id="file-progress" class="progress" value="0" max="100" style="display: none"></progress>
<input type="submit" class="btn btn-primary" id="next-step" value="$lang[next]" disabled />
<input type="hidden" name="next-import-step" value="globalValuesForm" />
<input type="hidden" name="interim-file" id="interim-file" value="" />
</form>
HTML;
self::$indiciaData['importerDropArea'] = '.dm-uploader';
return $r;
}
/**
* Fetch the HTML for the import reversal drop-down.
*
* @param array $options
* Options array for the control.
*
* @return string
* HTML.
*/
private static function importToReverseDropDown(array $options) {
iform_load_helpers(['report_helper']);
if (!function_exists('hostsite_get_user_field') || !hostsite_get_user_field('indicia_user_id')) {
return '';
}
// We only show user their own imports.
$indiciaUserID = hostsite_get_user_field('indicia_user_id');
$extraParams = [
'currentUser' => $indiciaUserID,
];
// This will be empty if importer is running on Warehouse.
// In that case all imports for the logged-in user will be shown.
if (!empty($options['website_id'])) {
$extraParams = array_merge(
$extraParams, ['website_id' => $options['website_id']]
);
}
// Get a list of imports that are reversible.
$lookupData = report_helper::get_report_data([
'readAuth' => $options['readAuth'],
'dataSource' => 'library/imports/reversible_imports_list',
'extraParams' => $extraParams,
]);
self::addLanguageStringsToJs('import_helper_2', [
'are_you_sure_reverse' => 'Are you sure you want to reverse the selected import?',
]);
$lang = [
'select_a_previous_import_to_reverse' => lang::get('Or select a previous import to reverse'),
'no_previous_imports_available_for_you_to_reverse' => lang::get('There are no previous imports available for you to reverse'),
'imports_are_not_always_reversible' => lang::get('Please note that imports are not always reversible.'),
'non_reversible_import_reasons' => lang::get('This may include old imports, imports that have been updated by another import,
or imports where a reversal has already been attempted.'),
'reverse_import' => lang::get('Reverse import'),
'new_records_will_be_reversed' => lang::get('Only new records created by the original import will be reversed.'),
'updated_records_will_not_be_reversed' => lang::get('Any records that were selected for an update or deletion during that import will not be reversed.'),
];
// Construct label for the drop-down from the date/time and import_guid.
$reversableImports = [];
foreach ($lookupData as $importRow) {
$affected = $importRow['inserted'] + $importRow['updated'];
$reversableImports[$importRow['import_guid']] = lang::get('{1}, {2} rows (Import ID: {3}', $importRow['import_date_time'], $affected, $importRow['import_guid']);
}
$r = <<<HTML
<hr>
<p>
$lang[select_a_previous_import_to_reverse]
</p>
HTML;
if (empty($reversableImports)) {
$r .= <<<HTML
<div>
<em>
$lang[no_previous_imports_available_for_you_to_reverse]
</em>
</div>
HTML;
}
// If there are some reversible imports, show user a drop-down of imports
// and a run the reverse button.
else {
$r .= <<<HTML
<form id="reverse-import-form" method="POST">
HTML;
$r .= data_entry_helper::select([
'id' => 'reverse-guid',
'fieldname' => 'reverse-guid',
'label' => lang::get('Import to reverse'),
'lookupValues' => $reversableImports,
'blankText' => lang::get('<please select>'),
]);
$r .= <<<HTML
<p>
<small>
<em>
$lang[imports_are_not_always_reversible]
<br>
$lang[non_reversible_import_reasons]
</em>
</small>
</p>
<input type="submit" class="btn btn-primary" id="run-reverse" value="$lang[reverse_import]" disabled />
<p style="display:none;" class="reverse-instructions-1">
<strong>
$lang[new_records_will_be_reversed]
<br>
$lang[updated_records_will_not_be_reversed]
</strong>
</p>
<input type="hidden" name="next-import-step" value="reversalModeWarningOrSkipToResult" />
</form>
HTML;
}
return $r;
}
/**
* Allow user to select import reverse options.
*
* If applicable, allow the user to select whether to reverse all data,
* or just data that has not been changed since importing.
* Skips directly to result page if there is nothing to ask the user.
*
* @param array $options
* Options array for the control.
*
* @return string
* HTML.
*/
private static function reversalModeWarningOrSkipToResult(array $options) {
iform_load_helpers(['report_helper']);
self::addLanguageStringsToJs('import_helper_2', [
'abort' => 'Abort',
'continue' => 'Continue',
'are_you_sure_reverse' => 'Are you sure you wish to continue?',
]);
$extraParams = [];
if (!empty($_POST['reverse-guid'])) {
$extraParams['import_guid'] = $_POST['reverse-guid'];
}
else {
$extraParams['import_guid'] = '';
}
// Has any of the data been changed since the import was done.
$smpsChangedSinceImport = report_helper::get_report_data([
'dataSource' => 'library/imports/changed_smps_since_import',
'readAuth' => $options['readAuth'],
'extraParams' => $extraParams,
]);
$occsChangedSinceImport = report_helper::get_report_data([
'dataSource' => 'library/imports/changed_occs_since_import',
'readAuth' => $options['readAuth'],
'extraParams' => $extraParams,
]);
// If no changes have been made to the imported data,
// then we can skip straight to the result page.
if (empty($smpsChangedSinceImport) && empty($occsChangedSinceImport)) {
return self::reversalResult($options);
}
$lang = [
'changes_have_been_made' => lang::get('Changes have been made to the data since it was imported.'),
'how_do_you_wish_to_proceed' => lang::get('How do you wish to proceed?'),
'reverse_all_rows' => lang::get('Reverse all rows'),
'reverse_unchanged_rows' => lang::get('Reverse unchanged rows'),
'abort_the_reverse' => lang::get('Abort the reverse'),
'continue' => lang::get('Continue'),
];
// If import data has been changed since import, then give the user
// the following options.
// Reverse all data, reverse only unchanged data, or abort reverse.
$reverseGuid = $_POST['reverse-guid'];
$r = <<<HTML
<form id="reversal-mode-warning-form" method="POST">
<h3>
$lang[changes_have_been_made]
</h3>
<h4>
$lang[how_do_you_wish_to_proceed]
</h4>
<div>
<p>
<label for="reverse_all" style="display: inline-flex; align-items: center;">
<input style="margin: 0 0.5em 0;" type="radio" id="reverse_all" name="reverse-mode" value="reverse_all">
<em>
$lang[reverse_all_rows]
</em>
</label>
</p>
<p>
<label for="do_not_reverse_updated" style="display: inline-flex; align-items: center;">
<input style="margin: 0 0.5em 0;" type="radio" id="do_not_reverse_updated" name="reverse-mode" value="do_not_reverse_updated">
<em>
$lang[reverse_unchanged_rows]
</em>
</label>
</p>
<p>
<label for="abort_reverse" style="display: inline-flex; align-items: center;">
<input style="margin: 0 0.5em 0;" type="radio" id="abort_reverse" name="reverse-mode" value="abort_reverse">
<em>
$lang[abort_the_reverse]
</em>
</label>
</p>
<br>
<input type="submit" class="btn btn-primary" id="run-reverse" value="$lang[continue]" />
<input type="hidden" name="next-import-step" value="reversalResult" />
<input type="hidden" name="reverse-guid" value="$reverseGuid" />
</div>
</form>
HTML;
return $r;
}
/**
* Send reversal information to the Warehouse, and display result.
*
* @param array $options
* Options array for the control.
*
* @return string
* HTML.
*/
private static function reversalResult(array $options) {
$indiciaUserID = hostsite_get_user_field('indicia_user_id');
$data['warehouse_user_id'] = $indiciaUserID;
$serviceUrl = self ::$base_url . 'index.php/services/import_2/import_reverse';
if (!empty($_POST['reverse-guid'])) {
$data['guid_to_reverse'] = $_POST['reverse-guid'];
}
if (!empty($_POST['reverse-mode'])) {
$data['reverse_mode'] = $_POST['reverse-mode'];
}
$response = self::http_post($serviceUrl, $data, FALSE);
$output = json_decode($response['output'], TRUE);
$reverseGuid = $_POST['reverse-guid'];
$r = <<<HTML
<form id="reversal-result-form" method="POST">
HTML;
global $indicia_templates;
if (!isset($response['result']) || $output['status'] !== 'OK') {
$printedResponse = empty($output['msg']) ? print_r($response, TRUE) : $output['msg'];
$r .= str_replace('{message}', 'An error has occurred during the import reversal.', $indicia_templates['warningBox']);
$r .= <<<HTML
<p>
<em>The response from the database is:</em>
</p>
<pre>$printedResponse</pre>
HTML;
}
else {
// Result includes links to files which contain changed and unchanged
// sample/occurrence rows.
$samplesDetails = '<p>' . implode('</p><p>', $output['samplesDetails']);
$occurrencesDetails = '<p>' . implode('</p><p>', $output['occurrencesDetails']);
$r .= <<<HTML
<h3>The reversal of import $reverseGuid is complete</h3>
<h4>$output[samplesOutcome]</h4>
$samplesDetails
<h4>$output[occurrencesOutcome]</h4>
$occurrencesDetails
HTML;
}
$r .= <<<HTML
<br>
<div>
<input type="submit" class="btn btn-primary" id="return-to-start" value="Return to start" />
<input type="hidden" name="next-import-step" value="fileSelectForm" />
</div>
</form>
HTML;
return $r;
}
/**
* Fetch the HTML form for the settings form that captures global values.
*
* Global values are imported data values that apply to every record in the
* impore.
*
* @param array $options
* Options array for the control.
*
* @return string
* HTML.
*/
private static function globalValuesForm(array $options) {
self::addLanguageStringsToJs('import_helper_2', [
'backgroundProcessingDone' => 'Background processing done',
'extractingFile' => 'Extracting the data from the Zip file.',
'errorExtractingZip' => 'An error occurred on the server whilst extracting the Zip file',
'errorUploadingFile' => 'An error occurred on the server whilst uploading the file',
'fileExtracted' => 'Data extracted from Zip file.',
'fileUploaded' => 'File uploaded to the server.',
'loadingRecords' => 'Loading records into temporary processing area.',
'loaded' => 'Records loaded ready for matching.',
'preparingToLoadRecords' => 'Preparing to load records.',
'uploadError' => 'Upload error',
'uploadingFile' => 'Uploading the file to the server.',
]);
$lang = [
'backgroundProcessing' => lang::get('Background processing in progress...'),
'instructions' => lang::get($options['globalValuesFormIntro']),
'moreInfo' => lang::get('More info...'),
'next' => lang::get('Next step'),
'setting' => lang::get('Setting'),
'settingsFromTemplate' => lang::get('The following settings required for this page are being loaded from the selected template.'),
'title' => lang::get('Import settings'),
'value' => lang::get('Value'),
];
$template = self::loadSelectedTemplate($options);
if (!empty($template) && !empty($template['global_values'])) {
// Merge the template global values into the configuration's fixed
// values.
$globalValuesFromTemplate = json_decode($template['global_values'], TRUE);
$options['fixedValues'] = array_merge(
$options['fixedValues'],
$globalValuesFromTemplate
);
}
// Find the controls that we can accept global values for, depending on the
// entity we are importing into.
$formArray = self::getGlobalValuesFormControlArray($options);
$form = self::globalValuesFormControls($formArray, $options);
self::$indiciaData['processUploadedInterimFile'] = $_POST['interim-file'];
return <<<HTML
<h3>$lang[title]</h3>
<p>$lang[instructions]</p>
<form id="settings-form" method="POST">
$form
<div class="panel panel-info background-processing">
<div class="panel-heading">
<span>$lang[backgroundProcessing]</span>
<progress id="file-progress" class="progress" value="0" max="100"></progress>
<br/><a data-toggle="collapse" class="small" href="#background-extra">$lang[moreInfo]</a>
</div>
<div id="background-extra" class="panel-body panel-collapse collapse"></div>
</div>
<input type="submit" class="btn btn-primary" id="next-step" value="$lang[next]" disabled />
<input type="hidden" name="next-import-step" value="mappingsForm" />
<input type="hidden" name="data-file" id="data-file" value="" />
</form>
HTML;
}
/**
* Retreives the array of control definitions for the global values form.
*
* Fetches the information from the warehouse model.
*
* @param array $options
* Importer options array.
*
* @return array
* List of control info.
*/
private static function getGlobalValuesFormControlArray($options) {
$response = self::cacheGet(['entityImportSettings' => $options['entity']]);
if ($response === FALSE) {
$request = parent::$base_url . "index.php/services/import_2/get_globalvalues_form/" . $options['entity'];
$request .= '?' . self::array_to_query_string($options['readAuth']);
$response = self::http_post($request, []);
if (!isset($response['error'])) {
self::cacheSet(['entityImportSettings' => $options['entity']], json_encode($response));
}
}
else {
$response = json_decode($response, TRUE);
}
return !empty($response['output']) ? json_decode($response['output'], TRUE) : [];
}
/**
* Returns a list of controls for the global values form.
*
* Controls in the list will depend on the entity settings on the warehouse.
*/
private static function globalValuesFormControls($formArray, $options) {
$r = '';
$options['helpText'] = TRUE;
$options['form'] = $formArray;
$options['param_lookup_extras'] = [];
$visibleControlsFound = FALSE;
$tools = [];
global $indicia_templates;
foreach ($formArray as $key => $info) {
// @todo Description should really have i18n inside getParamsFormControl,
// though this would require changing how the show unrestricted button UI
// is done.
$info['description'] = lang::get($info['description']);
$unrestrictedControl = NULL;
if (isset($options['fixedValueDefaults'][$key])) {
$info['default'] = $options['fixedValueDefaults'][$key];
}
if (!empty($options['fixedValues'][$key])) {
$optionList = explode(';', $options['fixedValues'][$key]);
// * indicates user needs option to select from full list.
if (in_array('*', $optionList)) {
unset($optionList[array_search('*', $optionList)]);
$origDisplayLabel = $info['display'];
$origDescription = $info['description'];
$info['display'] .= ' (' . lang::get('unrestricted') . ')';
$info['description'] .= ' ' . lang::get('Showing all available options.') . ' ' .
"<button type=\"button\" class=\"show-restricted $indicia_templates[buttonDefaultClass] $indicia_templates[buttonSmallClass]\">" . lang::get('Show preferred options') . '</button>';
$unrestrictedControl = self::getParamsFormControl("$key-unrestricted", $info, $options, $tools);
$info['display'] = $origDisplayLabel;
$info['description'] = $origDescription . ' ' . lang::get('Currently only showing preferred options.') . ' ' .
"<button type=\"button\" class=\"show-unrestricted $indicia_templates[buttonDefaultClass] $indicia_templates[buttonSmallClass]\">" . lang::get('Show all options') . '</button>';
}
if (isset($info['lookup_values'])) {
$info['lookup_values'] = self::getRestrictedLookupValues($info['lookup_values'], $optionList);
}
elseif (isset($info['population_call'])) {
$tokens = explode(':', $info['population_call']);
// 3rd part of population call is the ID field ($tokens[2]).
if ($tokens[0] === 'direct') {
$options['param_lookup_extras'][$key] = ['query' => json_encode(['in' => [$tokens[2] => $optionList]])];
}
elseif ($tokens[0] === 'report') {
$options['param_lookup_extras'][$key] = [$tokens[2] => implode(',', $optionList)];
}
$options['param_lookup_extras'][$key]['sharing'] = 'editing';
}
if (count($optionList) === 1 && !$unrestrictedControl) {
$r .= data_entry_helper::hidden_text([
'fieldname' => $key,
'default' => $optionList[0],
]);
continue;
}
}
$r .= '<div class="restricted ctrl-cntr" >' . self::getParamsFormControl($key, $info, $options, $tools) . '</div>';
if ($unrestrictedControl) {
$r .= "<div class=\"unrestricted ctrl-cntr\" style=\"display: none\">$unrestrictedControl</div>";
}
$visibleControlsFound = TRUE;
}
$updateOrDeleteOptions = self::updateOrDeleteOptions($options);
$r .= $updateOrDeleteOptions['html'];
if ($updateOrDeleteOptions['visibleControls']) {
$visibleControlsFound = TRUE;
}
if (!$visibleControlsFound) {
// All controls had a fixed value provided in config or the loaded
// template, so show a message instead of the form.
$r .= '<p class="alert alert-info">' . lang::get('None of the import settings require your input, so click <strong>Next step</strong> when the background processing is complete.') . ' </p>';
}
return $r;
}
/**
* Adds controls allowing the user to enable updates or deletes.
*
* @param array $options
* Configuration options.
*
* @return array
* Entry containing the control HTML (html) and a boolean flag set to true
* if any of the controls are visible, as this affects the UI behaviour.
*/
private static function updateOrDeleteOptions(array $options) {
$html = '';
if (!empty($options['allowUpdates'])) {
$ctrlType = isset($options['fixedValues']['config:allowUpdates']) ? 'hidden_text' : 'checkbox';
$html .= data_entry_helper::$ctrlType([
'fieldname' => 'config:allowUpdates',
'label' => lang::get('Import file contains updates for existing data'),
'helpText' => lang::get('Tick this box if your import file contains updates for existing data.'),
'default' => isset($options['fixedValues']['config:allowUpdates']) ? $options['fixedValues']['config:allowUpdates'] : 0,
]);
if (!empty($options['allowDeletes'])) {
$ctrlType = isset($options['fixedValues']['config:allowDeletes']) ? 'hidden_text' : 'checkbox';
$html .= data_entry_helper::$ctrlType([
'fieldname' => 'config:allowDeletes',
'label' => lang::get('Import file contains a flag for deleting existing data'),
'helpText' => lang::get('Tick this box if your import file contains a flag for deleting existing data.'),
'default' => isset($options['fixedValues']['config:allowDeletes']) ? $options['fixedValues']['config:allowDeletes'] : 0,
]);
if (!isset($options['fixedValues']['config:allowDeletes'])) {
// If not set by the template, the UI should only enable the deletes
// control when updates are enabled.
data_entry_helper::$indiciaData['enableControlIf']['config:allowDeletes'] = ['config:allowUpdates' => ['1']];
}
}
}
return [
'html' => $html,
'visibleControls' => !isset($options['fixedValues']['config:allowUpdates']) || !isset($options['fixedValues']['config:allowDeletes']),
];
}
/**
* Processes a lookup_values string to only include those for provided keys.
*
* @param string $lookupValues
* A lookup values string, in format key:value,key:value.
* @param array $restrictToKeys
* A list of keys to include in the returned lookup values string. Keys can
* have the label specified if a colon appended.
*