Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
johnvanbreda committed Jun 17, 2024
2 parents 0e781f6 + 56138c2 commit fecf214
Show file tree
Hide file tree
Showing 34 changed files with 3,228 additions and 1,634 deletions.
118 changes: 104 additions & 14 deletions ElasticsearchProxyHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ public static function callMethod($method, $nid) {
case 'bulkmoveids':
return self::proxyBulkMoveIds($nid);

case 'bulkeditall':
return self::proxyBulkEditAll($nid);

case 'bulkeditids':
return self::proxyBulkEditIds($nid);

case 'clearcustomresults':
return self::proxyClearCustomResults($nid);

Expand Down Expand Up @@ -826,7 +832,11 @@ public static function getHttpRequestHeaders($config, $contentType = 'applicatio
$headers[] = 'Authorization: ' . implode(':', $tokens);
}
else {
$keyFile = \Drupal::service('file_system')->realpath("private://") . '/rsa_private.pem';
$keyFile = \Drupal::service('file_system')->realpath("private://") . '/private.key';
if (!file_exists($keyFile)) {
// Fall back on legacy setup.
$keyFile = \Drupal::service('file_system')->realpath("private://") . '/rsa_private.pem';
}
if (!file_exists($keyFile)) {
\Drupal::logger('iform')->error('Missing private key file for jwtUser Elasticsearch authentication.');
throw new ElasticsearchProxyAbort('Method not allowed as server configuration incomplete', 405);
Expand Down Expand Up @@ -1602,11 +1612,11 @@ private static function applyFlagFilter($flag, array $definition, array &$bool)
* Bool clauses that filters can be added to (e.g. $bool['must']).
*/
private static function applyUserFiltersSearchArea($definition, array &$bool) {
if (!empty($definition['searchArea'])) {
if (!empty($definition['searchArea']) || !empty($definition['bufferedSearchArea'])) {
$bool['must'][] = [
'geo_shape' => [
'location.geom' => [
'shape' => $definition['searchArea'],
'shape' => $definition['bufferedSearchArea'] ?? $definition['searchArea'],
'relation' => 'intersects',
],
],
Expand Down Expand Up @@ -2700,36 +2710,47 @@ private static function getDefinitionFilter($definition, array $params) {
}

/**
* Receives a list of IDs to move between websites/datasets.
*
* Used by the recordsMover button.
* Either bulk move or edit a list of IDs.
*
* @return string
* Response from the data_utils/bulk_move service.
* Response from the data_utils/bulk_move or bulk_edit service.
*/
private static function bulkMoveIds($nid, array $ids, $datasetMappings, $precheck) {
private static function bulkProcessIds($nid, array $ids, $service, array $data) {
// Now do the move on the warehouse.
iform_load_helpers(['helper_base']);
$request = helper_base::$base_url . "index.php/services/data_utils/bulk_move";
$request = helper_base::$base_url . "index.php/services/data_utils/$service";
$conn = iform_get_connection_details($nid);
$auth = helper_base::get_read_write_auth($conn['website_id'], $conn['password']);
$postargs = helper_base::array_to_query_string(array_merge([
'occurrence:ids' => implode(',', $ids),
'datasetMappings' => $datasetMappings,
'precheck' => $precheck ? 't' : 'f',
], $auth['write_tokens']), TRUE);
], $data, $auth['write_tokens']), TRUE);
$response = helper_base::http_post($request, $postargs, FALSE);
// The response should be in JSON.
header('Content-type: application/json');
$output = json_decode($response['output']);
if (!$precheck && $output->code === 200) {
if (!($data['precheck'] ?? 'f' === 't') && isset($output->code) && $output->code === 200) {
// Set website ID to 0, basically disabling the ES copy of the record
// until a proper update with correct taxonomy information comes through.
// until a proper update with correct information comes through.
self::internalModifyListOnEs($ids, [], 0);
}
return $response['output'];
}

/**
* Receives a list of IDs to move between websites/datasets.
*
* Used by the recordsMover button.
*
* @return string
* Response from the data_utils/bulk_move service.
*/
private static function bulkMoveIds($nid, array $ids, $datasetMappings, $precheck) {
return self::bulkProcessIds($nid, $ids, 'bulk_move', [
'datasetMappings' => $datasetMappings,
'precheck' => $precheck ? 't' : 'f',
]);
}

/**
* Receives a filter defining records to move between websites/datasets.
*
Expand Down Expand Up @@ -2768,6 +2789,75 @@ private static function proxyBulkMoveIds($nid) {
return self::bulkMoveIds($nid, explode(',', $_POST['occurrence:ids']), $_POST['datasetMappings'], !empty($_POST['precheck']));
}

/**
* Bulk edit a list of occurrence IDs.
*
* @param int $nid
* Node ID.
* @param array $ids
* List of occurrence IDs to edit.
* @param array $updates
* Key/value pairs for fields to change. Currently supports recorder_name,
* location_name, date and sref + sref_system.
* @param array $options
* Key/value pairs for any options to pass through.
*
* @return array
* Response data containing information about affected records.
*/
private static function bulkEditIds($nid, array $ids, array $updates, array $options) {
$response = self::bulkProcessIds($nid, $ids, 'bulk_edit', [
'updates' => json_encode($updates),
'options' => json_encode($options),
]);
return json_decode($response, TRUE);
}

/**
* Bulk edit all records in the current filter.
*
* @param int $nid
* Node ID.
*
* @return array
* Response data containing information about affected records or next
* batch to fetch if paging.
*/
private static function proxyBulkEditAll($nid) {
$batchInfo = self::getOccurrenceIdPageFromFilter(
$nid,
$_POST['occurrence:idsFromElasticFilter'],
$_POST['search_after'] ?? NULL,
);
if (empty($batchInfo['ids'])) {
return [
'code' => 204,
'message' => 'No Content',
];
}
$response = self::bulkEditIds($nid, $batchInfo['ids'], $_POST['updates'], $_POST['options'] ?? []);
// Attach the search_after pagination info to the response.
if ($response['code'] === 200 && !empty($batchInfo['search_after'])) {
// Set pagination info, but not if empty array returned (which implies
// all done).
$response['search_after'] = $batchInfo['search_after'];
}
return $response;
}

/**
* Bulk edit a list of selected records.
*
* @param int $nid
* Node ID.
*
* @return array
* Response data containing information about affected records.
*/
private static function proxyBulkEditIds($nid) {
return self::bulkEditIds($nid, explode(',', $_POST['occurrence:ids']), $_POST['updates'], $_POST['options'] ?? []);
}

/**
* Proxy method that receives a filter and clears the user's custom flags.
*
Expand Down
148 changes: 143 additions & 5 deletions ElasticsearchReportHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,114 @@ public static function enableElasticsearchProxy($nid = NULL) {
}
catch (Exception $e) {
self::$proxyEnableFailed = TRUE;
\Drupal::logger('iform')->error('Elasticsearch proxy enable failed: ' . $e->getMessage());
}
}
return self::$proxyEnabled;
}

/**
* An Elasticsearch bulk editor tool.
*
* @return string
* Button container HTML.
*
* @link https://indicia-docs.readthedocs.io/en/latest/site-building/iform/helpers/elasticsearch-report-helper.html#elasticsearchreporthelper-bulkeditor
*/
public static function bulkEditor(array $options) {
self::checkOptions(
'bulkEditor',
$options,
['linkToDataControl'],
[]
);
$options = array_merge([
'caption' => 'Bulk edit records',
'restrictToOwnData' => TRUE,
], $options);
$dataOptions = helper_base::getOptionsForJs($options, [
'id',
'linkToDataControl',
'restrictToOwnData',
], TRUE);
helper_base::addLanguageStringsToJs('bulkEditor', [
'allowSampleSplitting' => 'Allow sample splitting?',
'bulkEditorDialogMessageAll' => 'You are about to edit the entire list of {1} records.',
'bulkEditorDialogMessageSelected' => 'You are about to edit {1} selected records.',
'bulkEditProgress' => 'Edited {samples} samples and {occurrences} occurrences.',
'cannotProceed' => 'Cannot proceed',
'confirm' => 'Confirm',
'done' => 'Records successfully edited. They will now be processed so they are available with their new values shortly.',
'error' => 'An error occurred whilst trying to edit the records.',
'errorEditNotFilteredToCurrentUser' => 'The records cannot be edited because the current page is not filtered to limit the records to only your data.',
'preparing' => 'Preparing to edit the records...',
'promptAllowSampleSplit' => '<p>The list of records to update contains occurrences which belong to samples that contain other occurrences which are not being updated. ' .
'For example, sample {1} contains an occurrence {2} which is being updated, but it also contains occurrence {3} which is not being updated.</p>' .
'<p>Please confirm that you would like to split the samples so that the data values for the list of records you are editing can be updated without affecting other occurrences in the same samples.</p>',
'warningNoChanges' => 'Please define at least one field value that you would like to change when bulk editing the records.',
'warningNothingToDo' => 'There are no selected records to edit.',
]);
$lang = [
'bulkEditRecords' => lang::get($options['caption']),
'cancel' => lang::get('Cancel'),
'close' => lang::get('Close'),
'editing' => lang::get('Editing records'),
'editInstructions' => 'Specify values to apply to all the edited records in the following controls, or leave blank for the data values to remain unchanged.',
'proceed' => lang::get('Proceed'),
];
helper_base::add_resource('fancybox');
$recorderNameControl = data_entry_helper::text_input([
'fieldname' => 'edit-recorder-name',
'label' => lang::get('Recorder name'),
'attributes' => ['placeholder' => lang::get('value not changed')],
]);
$dateControl = data_entry_helper::text_input([
'fieldname' => 'edit-date',
'label' => lang::get('Date'),
'attributes' => ['placeholder' => lang::get('value not changed')],
]);
$locationNameControl = data_entry_helper::text_input([
'fieldname' => 'edit-location-name',
'label' => lang::get('Location name'),
'attributes' => ['placeholder' => lang::get('value not changed')],
]);
$srefControl = data_entry_helper::sref_and_system([
'fieldname' => 'edit-sref',
'label' => lang::get('Spatial reference'),
'attributes' => ['placeholder' => lang::get('value not changed')],
'findMeButton' => FALSE,
]);
global $indicia_templates;
$html = <<<HTML
<button type="button" class="bulk-edit-records-btn $indicia_templates[buttonHighlightedClass]">$lang[bulkEditRecords]</button>
<div style="display: none">
<div id="$options[id]-dlg" class="bulk-editor-dlg">
<div class="pre-bulk-edit-info">
<h2>$lang[bulkEditRecords]</h2>
<p class="message"></p>
<p>$lang[editInstructions]</p>
$recorderNameControl
$dateControl
$locationNameControl
$srefControl
<div class="form-buttons">
<button type="button" class="$indicia_templates[buttonHighlightedClass] proceed-bulk-edit">$lang[proceed]</button>
<button type="button" class="$indicia_templates[buttonHighlightedClass] close-bulk-edit-dlg">$lang[cancel]</button>
</div>
</div>
<div class="post-bulk-edit-info">
<h2>$lang[editing]</h2>
<div class="output"></div>
<div class="form-buttons">
<button type="button" class="$indicia_templates[buttonHighlightedClass] close-bulk-edit-dlg" disabled="disabled">$lang[close]</button>
</div>
</div>
</div>
</div>
HTML;
return self::getControlContainer('bulkEditor', $options, $dataOptions, $html);
}

/**
* An Elasticsearch records card gallery.
*
Expand Down Expand Up @@ -665,9 +768,40 @@ public static function download(array $options) {
return self::getControlContainer('esDownload', $options, $dataOptions, $html);
}

/**
* A scale for grid square opacity.
*
* Showing the number of records for each level.
*
* @param array $options
* Control options.
*
* @return string
* Scale container HTML.
*
* @link https://indicia-docs.readthedocs.io/en/latest/site-building/iform/helpers/elasticsearch-report-helper.html#elasticsearchreporthelper-gridsquareopacityscale
*/
public static function gridSquareOpacityScale(array $options) {
self::checkOptions('gridSquareOpacityScale', $options,
['linkToDataControl', 'layer'],
[]
);
helper_base::addLanguageStringsToJs('gridSquareOpacityScale', [
'noOfRecords' => 'No. of records',
]);
$dataOptions = helper_base::getOptionsForJs($options, [
'id',
'linkToDataControl',
'layer',
], TRUE);
return self::getControlContainer('gridSquareOpacityScale', $options, $dataOptions);
}

/**
* Integrates the page with groups (activities).
*
* @param array $options
* Control options.
* @param bool $checkPage
* Set to false to disable checking that the current page path is an iform
* page linked to the group.
Expand Down Expand Up @@ -835,6 +969,7 @@ private static function internalLocationSelect(array $options, $addGeomHiddenInp
'orderby' => 'name',
], $options['extraParams'], $options['readAuth']);
$baseId = $options['id'];
$selectClass = $options['class'];
foreach ($typeIds as $idx => $typeId) {
$options['extraParams']['location_type_id'] = $typeId;
if (count($typeIds) > 1) {
Expand All @@ -843,7 +978,9 @@ private static function internalLocationSelect(array $options, $addGeomHiddenInp
}
if ($idx > 0) {
$options['parentControlId'] = "$baseId-" . ($idx - 1);
// We don't want the query to apply to the child drop-downs ($idx > 0), as the query can't apply to both parent/child, as they are very different.
// We don't want the query to apply to the child drop-downs ($idx > 0),
// as the query can't apply to both parent/child, as they are very
// different.
unset($options['extraParams']['query']);
if ($idx === 1) {
$options['parentControlLabel'] = $options['label'];
Expand All @@ -855,7 +992,7 @@ private static function internalLocationSelect(array $options, $addGeomHiddenInp
// If locations are unindexed we need a place to store the geometry for
// filtering.
if ($addGeomHiddenInput) {
$r .= "<input type=\"hidden\" id=\"$options[id]-geom\" class=\"es-filter-param $options[class]-geom\" data-es-bool-clause=\"must\">";
$r .= "<input type=\"hidden\" id=\"$options[id]-geom\" class=\"es-filter-param $selectClass-geom\" data-es-bool-clause=\"must\">";
}
}
return "<div class=\"location-select-cntr\">$r</div>";
Expand Down Expand Up @@ -1257,6 +1394,7 @@ public static function recordDetails(array $options) {
helper_base::addLanguageStringsToJs('recordDetails', [
'actionByPersonOnDate' => '{1} by {2} on {3}',
'comment' => 'Comment',
'recordEntered' => 'Record entered',
'redetermination' => 'Redetermination',
'verificationDecision' => 'Verification',
]);
Expand Down Expand Up @@ -1325,15 +1463,15 @@ public static function recordsMover(array $options) {
], TRUE);
helper_base::addLanguageStringsToJs('recordsMover', [
'cannotProceed' => 'Cannot proceed',
'done' => 'Records successfully moved. They will be processed so they are available in their new location shortly.',
'done' => 'Records successfully moved. They will now be processed so they are available in their new location shortly.',
'error' => 'An error occurred whilst trying to move the records.',
'errorNotFilteredToCurrentUser' => 'The records cannot be moved because the current page is not filtered to limit the records to only your data.',
'moveProgress' => 'Moved {samples} samples and {occurrences} occurrences.',
'moving' => 'Moving the records...',
'precheckProgress' => 'Checked {samples} samples and {occurrences} occurrences.',
'preparing' => 'Preparing to move the records...',
'recordsMoverDialogMessageSelected' => 'You are about to move {1} selected records.',
'recordsMoverDialogMessageAll' => 'You are about to move the entire list of {1} records.',
'recordsMoverDialogMessageSelected' => 'You are about to move {1} selected records.',
'warningNothingToDo' => 'There are no selected records to move.',
]);
$lang = [
Expand All @@ -1348,7 +1486,7 @@ public static function recordsMover(array $options) {
$html = <<<HTML
<button type="button" class="move-records-btn $indicia_templates[buttonHighlightedClass]">$lang[moveRecords]</button>
<div style="display: none">
<div id="$options[id]-dlg">
<div id="$options[id]-dlg" class="records-mover-dlg">
<div class="pre-move-info">
<h2>$lang[moveRecords]</h2>
<p class="message"></p>
Expand Down
Loading

0 comments on commit fecf214

Please sign in to comment.