diff --git a/ElasticsearchProxyHelper.php b/ElasticsearchProxyHelper.php index b74525f8..a1ce14bb 100644 --- a/ElasticsearchProxyHelper.php +++ b/ElasticsearchProxyHelper.php @@ -1372,7 +1372,7 @@ private static function applyGroupFilter(array $readAuth, $groupFilter, array &$ * @param array $bool * ES bool query definintion. */ - private static function applyFilterDef(array $readAuth, array $definition, array &$bool) { + public static function applyFilterDef(array $readAuth, array $definition, array &$bool) { self::convertLocationListToSearchArea($definition, $readAuth); self::applyUserFiltersTaxonGroupList($definition, $bool); self::applyUserFiltersTaxaTaxonList($definition, $bool, $readAuth); @@ -1420,16 +1420,25 @@ private static function convertLocationListToSearchArea(array &$definition, arra 'location_id', ]); if (!empty($filter)) { - require_once 'report_helper.php'; - $boundaryData = report_helper::get_report_data([ - 'dataSource' => '/library/locations/locations_combined_boundary_transformed', - 'extraParams' => [ - 'location_ids' => $filter['value'], - ], - 'readAuth' => $readAuth, - 'caching' => TRUE, - ]); - $definition['searchArea'] = $boundaryData[0]['geom']; + if (defined('KOHANA')) { + // Use direct access as on warehouse. + $polygon = warehouseEsFilters::getCombinedBoundaryData($filter['value']); + } + else { + // API access as on client. + require_once 'report_helper.php'; + $boundaryData = report_helper::get_report_data([ + 'dataSource' => '/library/locations/locations_combined_boundary_transformed', + 'extraParams' => [ + 'location_ids' => $filter['value'], + ], + 'readAuth' => $readAuth, + 'caching' => TRUE, + 'cachePerUser' => FALSE, + ]); + $polygon = $boundaryData[0]['geom']; + } + $definition['searchArea'] = $polygon; } } @@ -1467,14 +1476,21 @@ private static function applyUserFiltersTaxonGroupList(array $definition, array */ private static function applyTaxonomyFilter(array &$bool, array $readAuth, $filterField, $filterValues) { // Convert the IDs to external keys, stored in ES as taxon_ids. - $taxonData = helper_base::get_population_data([ - 'report' => 'library/taxa/convert_ids_to_external_keys', - 'extraParams' => [ - $filterField => $filterValues, - 'master_checklist_id' => hostsite_get_config_value('iform', 'master_checklist_id', 0), - ] + $readAuth, - 'cachePerUser' => FALSE, - ]); + if (defined('KOHANA')) { + // Use direct access as on warehouse. + $taxonData = warehouseEsFilters::taxonIdsToExternalKeys($filterField, $filterValues); + } + else { + // API access as on client. + $taxonData = helper_base::get_population_data([ + 'report' => 'library/taxa/convert_ids_to_external_keys', + 'extraParams' => [ + $filterField => $filterValues, + 'master_checklist_id' => hostsite_get_config_value('iform', 'master_checklist_id', 0), + ] + $readAuth, + 'cachePerUser' => FALSE, + ]); + } $keys = []; foreach ($taxonData as $taxon) { $keys[] = $taxon['external_key']; @@ -1680,16 +1696,23 @@ private static function applyUserFiltersIndexedLocationTypeList(array $definitio if (!empty($filter)) { // Convert the location type IDs to terms that are used in the ES // document. - $typeRows = helper_base::get_population_data([ - 'table' => 'termlists_term', - 'extraParams' => [ - 'id' => $filter, - 'view' => 'cache', - ] + $readAuth, - ]); + if (defined('KOHANA')) { + // Use direct access as on warehouse. + $typeTerms = warehouseEsFilters::getTermsFromIds($filter); + } + else { + // API access as on client. + $typeTerms = helper_base::get_population_data([ + 'table' => 'termlists_term', + 'extraParams' => [ + 'id' => $filter, + 'view' => 'cache', + ] + $readAuth, + ]); + } $types = []; - foreach ($typeRows as $typeRow) { - $types[] = $typeRow['term']; + foreach ($typeTerms as $typeTermRow) { + $types[] = $typeTermRow['term']; } if (count($types) > 0) { $bool['must'][] = [ @@ -2371,12 +2394,19 @@ private static function getLicenceCodes(array $readAuth) { 'open' => [], 'restricted' => [], ]; - $licences = helper_base::get_population_data([ - 'table' => 'licence', - 'extraParams' => $readAuth, - 'columns' => 'code,open', - 'cachePerUser' => FALSE, - ]); + if (defined('KOHANA')) { + // Direct access as on warehouse. + $licences = warehouseEsFilters::getLicences(); + } + else { + // API access as on client. + $licences = helper_base::get_population_data([ + 'table' => 'licence', + 'extraParams' => $readAuth, + 'columns' => 'code,open', + 'cachePerUser' => FALSE, + ]); + } foreach ($licences as $licence) { $r[$licence['open'] === 't' ? 'open' : 'restricted'][] = $licence['code']; } @@ -2512,7 +2542,6 @@ private static function applyUserFiltersLicences(array $definition, array &$bool */ private static function applyUserFiltersCoordinatePrecision(array $definition, array &$bool) { $filter = self::getDefinitionFilter($definition, ['coordinate_precision']); - \Drupal::logger('iform')->notice(var_export($filter, TRUE)); if (!empty($filter)) { // Default is same as or better than. $filter['op'] = $filter['op'] ?? '<='; @@ -2654,16 +2683,23 @@ private static function applyUserFiltersTaxaScratchpadList(array $definition, ar 'taxa_scratchpad_list_id', ]); if (!empty($filter)) { - require_once 'report_helper.php'; - // Convert the IDs to external keys, stored in ES as taxon_ids. - $taxonData = report_helper::get_report_data([ - 'dataSource' => '/library/taxa/external_keys_for_scratchpad', - 'extraParams' => [ - 'scratchpad_list_id' => $filter['value'], - ], - 'readAuth' => $readAuth, - 'caching' => TRUE, - ]); + if (defined('KOHANA')) { + // Direct access as on warehouse. + $taxonData = warehouseEsFilters::getExternalKeysForTaxonScratchpad($filter['value']); + } + else { + require_once 'report_helper.php'; + // Convert the IDs to external keys, stored in ES as taxon_ids. + $taxonData = report_helper::get_report_data([ + 'dataSource' => '/library/taxa/external_keys_for_scratchpad', + 'extraParams' => [ + 'scratchpad_list_id' => $filter['value'], + ], + 'readAuth' => $readAuth, + 'caching' => TRUE, + 'cachePerUser' => FALSE, + ]); + } $keys = []; foreach ($taxonData as $taxon) { $keys[] = $taxon['external_key']; diff --git a/ElasticsearchReportHelper.php b/ElasticsearchReportHelper.php index 4b07c805..11ea2da6 100644 --- a/ElasticsearchReportHelper.php +++ b/ElasticsearchReportHelper.php @@ -668,12 +668,16 @@ public static function download(array $options) { /** * Integrates the page with groups (activities). * + * @param bool $checkPage + * Set to false to disable checking that the current page path is an iform + * page linked to the group. + * * @link https://indicia-docs.readthedocs.io/en/latest/site-building/iform/helpers/elasticsearch-report-helper.html#elasticsearchreporthelper-groupintegration * * @return string * Control HTML */ - public static function groupIntegration(array $options) { + public static function groupIntegration(array $options, $checkPage = TRUE) { $options = array_merge([ 'missingGroupIdBehaviour' => 'error', 'showGroupSummary' => FALSE, @@ -693,7 +697,7 @@ public static function groupIntegration(array $options) { return ''; } require_once 'prebuilt_forms/includes/groups.php'; - $member = group_authorise_group_id($group_id, $options['readAuth']); + $member = group_authorise_group_id($group_id, $options['readAuth'], $checkPage); $output = ''; if (!empty($group_id)) { // Apply filtering by group. @@ -723,21 +727,21 @@ public static function groupIntegration(array $options) { $output .= self::getGroupPageLinks($group, $options, $member); } } - } - $filterBoundaries = helper_base::get_population_data([ - 'report' => 'library/groups/group_boundary_transformed', - 'extraParams' => $options['readAuth'] + ['group_id' => $group_id], - 'cachePerUser' => FALSE, - ]); - if (count($filterBoundaries) > 0) { - helper_base::$indiciaData['reportBoundaries'] = []; - foreach ($filterBoundaries as $boundary) { - helper_base::$indiciaData['reportBoundaries'][] = $boundary['boundary']; - } - helper_base::$late_javascript .= << 'library/groups/group_boundary_transformed', + 'extraParams' => $options['readAuth'] + ['group_id' => $group_id], + 'cachePerUser' => FALSE, + ]); + if (count($filterBoundaries) > 0) { + helper_base::$indiciaData['reportBoundaries'] = []; + foreach ($filterBoundaries as $boundary) { + helper_base::$indiciaData['reportBoundaries'][] = $boundary['boundary']; + } + helper_base::$late_javascript .= << 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. + unset($options['extraParams']['query']); if ($idx === 1) { $options['parentControlLabel'] = $options['label']; $options['filterField'] = 'parent_id'; @@ -919,7 +925,7 @@ public static function leafletMap(array $options) { } } } - // Override map position from URL if needed. + // Override map position from URL if needed. // (e.g. this allows control via an iFrame). if (!empty($_GET['initialLat'])) { $options['initialLat'] = $_GET['initialLat']; @@ -2360,9 +2366,11 @@ private static function applySourceModeDefaultsTermAggregation(array &$options) * Options passed to the [source]. Will be modified as appropriate. */ private static function applySourceModeDefaults(array &$options) { - $method = 'applySourceModeDefaults' . ucfirst($options['mode']); - if (method_exists('ElasticsearchReportHelper', $method)) { - self::$method($options); + if (!empty($options['mode'])) { + $method = 'applySourceModeDefaults' . ucfirst($options['mode']); + if (method_exists('ElasticsearchReportHelper', $method)) { + self::$method($options); + } } } @@ -2472,9 +2480,10 @@ private static function convertValueToFilterList($report, array $params, $output * @param string $controlName * Control type name (e.g. source, dataGrid). * @param array $options - * Options passed to the control. If the control's @containerElement option + * Options passed to the control. If the control's containerElement option * is set then sets the required JavaScript to make the control inject - * itself into the existing element instead. + * itself into the existing element instead. Can include an option for the + * id and class to assign to the container if not using containerElement. * @param string $dataOptions * Options to store in the HTML data-idc-config attribute on the container. * These are made available to configure the JS behaviour of the control. @@ -2500,8 +2509,9 @@ private static function getControlContainer($controlName, array $options, $dataO $('#$options[id]').$initFn({}); JS; + $class = "idc-control idc-$controlName" . (empty($options['class']) ? '' : ' ' . $options['class']); return << +
$content
diff --git a/data_entry_helper.php b/data_entry_helper.php index f40951ed..9a99114e 100644 --- a/data_entry_helper.php +++ b/data_entry_helper.php @@ -2838,6 +2838,8 @@ public static function sref_and_system(array $options) { * Associative array with the key being the EPSG code for the system or * the notation abbreviation (e.g. OSGB), and the value being the * description to display. + * * blankText - adds an item to the start of the list for when there is no + * value. * * @return string * HTML to insert into the page for the spatial reference systems selection @@ -2856,6 +2858,13 @@ public static function sref_system_select(array $options) { ], $options); $options = self::check_options($options); $opts = ''; + if (!empty($options['blankText'])) { + $opts .= str_replace( + ['{value}', '{caption}', '{selected}', '{attribute_list}'], + ['', $options['blankText']], + $indicia_templates['select_item'] + ); + } foreach ($options['systems'] as $system => $caption) { $selected = ($options['default'] == $system ? 'selected' : ''); $opts .= str_replace( @@ -3836,7 +3845,21 @@ public static function species_checklist(array $options) { } // Load the full list of species for the grid, including the main checklist // plus any additional species in the reloaded occurrences. - $taxalist = self::get_species_checklist_taxa_list($options, $taxonRows); + $taxalist = self::getSpeciesChecklistTaxaList($options, $taxonRows); + $shiftedTtlIds = []; + if ((!empty($_GET['sample_id']) || !empty($_GET['occurrence_id'])) && + !empty($options['listId'])) { + // If using a static species list, load occurrences using a name with + // same meaning if original name not available. + $preferredFlagsByTtlId = self::getPreferredFlagsByTtlId($taxalist); + $shiftTaxaOntoNamesWithSameMeaningResults = self::shiftTaxaOntoNamesWithSameMeaning($options['readAuth'], $taxonRows, $preferredFlagsByTtlId); + $taxonRows = $shiftTaxaOntoNamesWithSameMeaningResults['taxonRows']; + $shiftedTtlIds = $shiftTaxaOntoNamesWithSameMeaningResults['shiftedTtlIds']; + } + // If we are use species grid name that wasn't original recorded name, inform the user of the recorded name as well. + // Run this code even when sample_id, occurrence_id not supplied otherwise {previously_recorded_as} replacement tokens + // appear on the screen. + $taxalist = self::addPreviousNameWarningToList($options['readAuth'], $shiftedTtlIds, $taxalist); // If we managed to read the species list data we can proceed. if (!array_key_exists('error', $taxalist)) { $attrOptions = [ @@ -4022,6 +4045,12 @@ public static function species_checklist(array $options) { } $row .= "\n"; $fieldname = "sc:$options[id]-$txIdx:$existingRecordId:present"; + // If an occurrence does not have its original name available, then an alternative name + // with same meaning can be used. + // Switch back to the old taxa_taxon_list_id (but leave the updated name) so we don't fire a redetermination. + if (!empty($shiftedTtlIds) && array_key_exists($taxon['taxa_taxon_list_id'], $shiftedTtlIds)) { + $taxon['taxa_taxon_list_id'] = $shiftedTtlIds[$taxon['taxa_taxon_list_id']]; + } if ($options['rowInclusionCheck'] === 'hasData') { $row .= ""; } @@ -4368,6 +4397,186 @@ public static function species_checklist(array $options) { } } + /** + * Display occurrence against a name with the same meaning. + * + * When taxa are loaded onto data entry page in edit mode, we need to display + * the occurrence against a different name if we can find one with the same + * meaning and the original is not available. + * + * @param array $readAuth + * Read authorisation tokens. + * @param array $allRequestedTaxa + * List of taxa taxon list ids and occurrence information if applicable + * for all taxa that are being requested by the grid (including preloaded + * plus taxa for existing occurrences). + * @param array $preferredByTtlId + * Preferred flag for all the taxa listed, keyed by taxa_taxon_list_id. + * + * @return array + */ + private static function shiftTaxaOntoNamesWithSameMeaning(array $readAuth, array $allRequestedTaxa, array $preferredFlagsByTtlId) { + $taxaWhichNeedTaxonMeaningIds = []; + // Reformat requested taxon details so they are keyed by taxa taxon list ID. + $allRequestedTaxaByTtlId = []; + foreach ($allRequestedTaxa as $requestedTaxonDetail) { + $allRequestedTaxaByTtlId[$requestedTaxonDetail['ttlId']] = $requestedTaxonDetail; + // If the row needs shifting, it will need a taxon meaning ID for comparison later. + if (!isset($requestedTaxonDetail['taxon_meaning_id']) && isset($requestedTaxonDetail['occId']) && !array_key_exists($requestedTaxonDetail['ttlId'], $preferredFlagsByTtlId)) { + $taxaWhichNeedTaxonMeaningIds[] = $requestedTaxonDetail['ttlId']; + } + } + // Taxa added to the list for occurrences which are not in the preloaded + // list and need shifting won't have a taxon meaning ID. + if (!empty($taxaWhichNeedTaxonMeaningIds)) { + $fetchTaxonMeaningIds = data_entry_helper::get_population_data([ + 'table' => 'taxa_taxon_list', + 'extraParams' => $readAuth + [ + 'query' => json_encode(['in' => ['id' => $taxaWhichNeedTaxonMeaningIds]]), + 'view' => 'detail', + ], + ]); + foreach ($fetchTaxonMeaningIds as $taxonMeaningInfo) { + $allRequestedTaxaByTtlId[$taxonMeaningInfo['id']]['taxon_meaning_id'] = $taxonMeaningInfo['taxon_meaning_id']; + } + } + // Keep track of processed rows. + $shiftedTtlIds = []; + // Cycle through all taxa relating to occurences so we can find ones + // without a taxa_taxon_list on the page then find a taxa_taxon_list_id we + // can actually use. + foreach ($allRequestedTaxa as &$possibleRowToShiftFrom) { + // Will need to shift if an existing occurrence and not in the list of + // taxa taxon lists available in the grid. + if (isset($possibleRowToShiftFrom['occId']) && !array_key_exists($possibleRowToShiftFrom['ttlId'], $preferredFlagsByTtlId)) { + // Cycle through all taxa again to find a name with the same meaning but isn't the same name + // The first time we do this, only look for preferred names as we want to use that if available + foreach ($allRequestedTaxa as &$possibleRowToShiftTo) { + // If row to shift has already been processed then don't do anymore + // work. This can break the loop when a hit found. + if (!in_array($possibleRowToShiftFrom['ttlId'], $shiftedTtlIds)) { + // If the meaning is a match between the two rows but not the same taxa_taxon_list_id + if ($allRequestedTaxaByTtlId[$possibleRowToShiftFrom['ttlId']]['taxon_meaning_id'] == $allRequestedTaxaByTtlId[$possibleRowToShiftTo['ttlId']]['taxon_meaning_id'] && + $possibleRowToShiftFrom['ttlId'] != $possibleRowToShiftTo['ttlId'] && + array_key_exists($possibleRowToShiftTo['ttlId'], $preferredFlagsByTtlId) && + // Note cannot use TRUE, as boolean is supplied as string + $preferredFlagsByTtlId[$possibleRowToShiftTo['ttlId']] == 't') { + self::applyChangesToSwitchedRows($possibleRowToShiftFrom, $possibleRowToShiftTo, $shiftedTtlIds); + } + } + } + // If we need didn't find a preferred name, then continue to find a common name/synonym in similar way. + foreach ($allRequestedTaxa as &$possibleRowToShiftTo) { + // If row to shift has already been processed then don't do anymore + // work. This can break the loop when a hit found. + if (!in_array($possibleRowToShiftFrom['ttlId'], $shiftedTtlIds)) { + if ($allRequestedTaxaByTtlId[$possibleRowToShiftFrom['ttlId']]['taxon_meaning_id'] == $allRequestedTaxaByTtlId[$possibleRowToShiftTo['ttlId']]['taxon_meaning_id'] && + $possibleRowToShiftFrom['ttlId'] != $possibleRowToShiftTo['ttlId'] && + array_key_exists($possibleRowToShiftTo['ttlId'], $preferredFlagsByTtlId) && + // Note cannot use TRUE as boolean is supplied as string + // Use != 't' instead of == 't' as it will catch all other cases if this result is corrupted for some reason + $preferredFlagsByTtlId[$possibleRowToShiftTo['ttlId']] != 't') { + self::applyChangesToSwitchedRows($possibleRowToShiftFrom, $possibleRowToShiftTo, $shiftedTtlIds); + } + } + } + } + } + $shiftTaxaOntoNamesWithSameMeaningResults=[]; + $shiftTaxaOntoNamesWithSameMeaningResults['taxonRows'] = $allRequestedTaxa; + $shiftTaxaOntoNamesWithSameMeaningResults['shiftedTtlIds'] = $shiftedTtlIds; + return $shiftTaxaOntoNamesWithSameMeaningResults; + } + + /** + * Get's the preferred flag for each taxon, keyed by taxa taxon list ID. + * + * @param array $taxonListItems + * The details of the taxa available to choose from. + * + * @return array + * Associative array of preferred flag values. + */ + private static function getPreferredFlagsByTtlId($taxonListItems) { + $preferredFlagsByTtlId = []; + // Create array where the taxa_taxon_list_id is the key to make processing easier. + foreach ($taxonListItems as $taxonListRowDetail) { + $preferredFlagsByTtlId[$taxonListRowDetail['taxa_taxon_list_id']] = $taxonListRowDetail['preferred']; + } + return $preferredFlagsByTtlId; + } + + /** + * Move occurrence onto another name with same meaning. + * + * Add occurrence data to the new row, unset from old row. + * + * @param array $rowToShiftFrom + * The original row we are moving the occurrence data from. + * @param array $rowToShiftTo + * The original row we are moving the occurrence data to. + * @param array $shiftedTtlIds + * Taxa_taxon_list_ids of species that have already been processed. + * + * @return array + */ + private static function applyChangesToSwitchedRows(&$rowToShiftFrom, &$rowToShiftTo, &$shiftedTtlIds) { + // Add the occurrence detail to the name we have found + $rowToShiftTo['loadedTxIdx'] = $rowToShiftFrom['loadedTxIdx']; + $rowToShiftTo['occId'] = $rowToShiftFrom['occId']; + $rowToShiftTo['smpIdx'] = $rowToShiftFrom['smpIdx']; + // Then remove the data relating to the occurrence from original row + unset($rowToShiftFrom['loadedTxIdx']); + unset($rowToShiftFrom['occId']); + unset($rowToShiftFrom['smpIdx']); + // Note that this row has now been processed. + $shiftedTtlIds[$rowToShiftTo['ttlId']] = $rowToShiftFrom['ttlId']; + } + + /** + * Add recorded name to species list if needed. + * + * If the species name available on screen is not the same as the recorded name, + * then inform the user of the recorded name. + * + * @param array @readAuth + * Authentication tokens for the Warehouse. + * @param array $shiftedTtlIds + * Array The array key is the displayed taxon's ttl ID, the value is the original recorded ttl ID). + * @param array $taxonListItems + * The details of the taxa available on screen. + * + * @return array + */ + private static function addPreviousNameWarningToList($readAuth, $shiftedTtlIds, $taxonListItems) { + // Get details of the original recorded taxa + $fullDetailsRecordedTaxa = data_entry_helper::get_population_data([ + 'table' => 'taxa_taxon_list', + 'extraParams' => $readAuth + [ + 'query' => json_encode(['in' => ['id' => $shiftedTtlIds]]), + 'view' => 'detail', + ], + ]); + // Format the output so that the ttl_id is the array key + $fullDetailsRecordedTaxaFormatted = []; + foreach ($fullDetailsRecordedTaxa as $fullDetailsRecordedTaxonRow) { + $fullDetailsRecordedTaxaFormatted[$fullDetailsRecordedTaxonRow['id']] = $fullDetailsRecordedTaxonRow; + } + foreach ($taxonListItems as &$taxonListItem) { + // If we find a name we are using that is not the original name on the occurrence + if (array_key_exists($taxonListItem['taxa_taxon_list_id'], $shiftedTtlIds)) { + // Then we are going make a label displaying the recorded name to the user. + // To get the name, we need to get the recorded ttl from the $shiftedTtlIds, + // then simply collect the taxon name from the array containing the taxon details. + $taxonListItem['previously_recorded_as'] = + $fullDetailsRecordedTaxaFormatted[$shiftedTtlIds[$taxonListItem['taxa_taxon_list_id']]]['taxon']; + } else { + $taxonListItem['previously_recorded_as'] = ''; + } + } + return $taxonListItems; + } + /** * Checks if a list of mediaTypes only contains images. * @@ -4814,7 +5023,7 @@ public static function species_checklist_filter_popup($options, $nameFilter) { * @param string $spatialRefPrecisionAttrId * Provide the ID of the attribute which defines the spatial ref precision * of each subSample where relevant. - * @param string $subSampleAttrs + * @param array $subSampleAttrs * Array of attribute IDs to load for the sub-samples. * * @return array @@ -4822,7 +5031,7 @@ public static function species_checklist_filter_popup($options, $nameFilter) { */ public static function preload_species_checklist_occurrences($sampleId, $readAuth, array $loadMedia, $extraParams, &$subSamples, $useSubSamples, $subSampleMethodID='', - $spatialRefPrecisionAttrId = NULL, $subSampleAttrs = []) { + $spatialRefPrecisionAttrId = NULL, array $subSampleAttrs = []) { $occurrenceIds = []; // don't load from the db if there are validation errors, since the $_POST will already contain all the // data we need. @@ -4848,9 +5057,16 @@ public static function preload_species_checklist_occurrences($sampleId, $readAut } } if ($useSubSamples) { - $extraParams += $readAuth + ['view' => 'detail','parent_id'=>$sampleId,'deleted' => 'f', 'orderby' => 'id', 'sortdir' => 'ASC']; - if($subSampleMethodID != '') + $extraParams += $readAuth + [ + 'view' => 'detail', + 'parent_id'=>$sampleId, + 'deleted' => 'f', + 'orderby' => 'id', + 'sortdir' => 'ASC', + ]; + if($subSampleMethodID != '') { $extraParams['sample_method_id'] = $subSampleMethodID; + } $params = array( 'table' => 'sample', 'extraParams' => $extraParams, @@ -5244,7 +5460,7 @@ private static function getSpeciesChecklistColResponsive($options, $column) { * @return array * The taxon list to use in the grid. */ - public static function get_species_checklist_taxa_list($options, &$taxonRows) { + public static function getSpeciesChecklistTaxaList($options, &$taxonRows) { // Get the list of species that are always added to the grid, by first // building a filter or using preloaded ones. if (!empty($options['preloadTaxa'])) { @@ -5272,7 +5488,10 @@ public static function get_species_checklist_taxa_list($options, &$taxonRows) { // Create a list of the rows we are going to add to the grid, with // the preloaded species names linked to them. if ($taxonFilter == $taxon['taxa_taxon_list_id']) { - $taxonRows[] = array('ttlId'=>$taxon['taxa_taxon_list_id']); + $taxonRows[] = [ + 'ttlId' => $taxon['taxa_taxon_list_id'], + 'taxon_meaning_id' => $taxon['taxon_meaning_id'], + ]; } } } @@ -5280,7 +5499,10 @@ public static function get_species_checklist_taxa_list($options, &$taxonRows) { else { foreach ($taxalist as $taxon) { // create a list of the rows we are going to add to the grid, with the preloaded species names linked to them - $taxonRows[] = array('ttlId' => $taxon['taxa_taxon_list_id']); + $taxonRows[] = [ + 'ttlId' => $taxon['taxa_taxon_list_id'], + 'taxon_meaning_id' => $taxon['taxon_meaning_id'], + ]; } } // If there are any existing records to add to the list from the lookup @@ -6817,7 +7039,7 @@ protected static function check_options($options) { * ID of the database record to load. * @param string $view * Name of the view to load attributes from, normally 'list' or 'detail'. - * @param bool $sharing + * @param bool|string $sharing * Defaults to false. If set to the name of a sharing task (reporting, * peer_review, verification, data_flow, moderation or editing), then the * record can be loaded from another client website if a sharing agreement @@ -6876,7 +7098,7 @@ private static function getControlFieldKeyMappings() { * ID of the database record to load. * @param string $view * Name of the view to load attributes from, normally 'list' or 'detail'. - * @param boolean $sharing + * @param boolean|string $sharing * Defaults to false. If set to the name of a sharing task (reporting, * peer_review, verification, data_flow, moderation or editing), then the * record can be loaded from another client website if a sharing agreement @@ -9108,7 +9330,7 @@ public static function getAttributes($options, $indexedArray = TRUE, $sharing = } elseif ($options['attrtable'] !== 'person_attribute' && $options['attrtable'] !== 'taxa_taxon_list_attribute') { $surveys = array(NULL); - if (isset($options['survey_id'])) { + if (!empty($options['survey_id'])) { $surveys[] = $options['survey_id']; $survey = data_entry_helper::get_population_data(array( 'table' => 'survey', diff --git a/prebuilt_forms/css/ukbms_timed_observations.css b/prebuilt_forms/css/ukbms_timed_observations.css deleted file mode 100644 index e9b93b7e..00000000 --- a/prebuilt_forms/css/ukbms_timed_observations.css +++ /dev/null @@ -1,67 +0,0 @@ -label { - width: 140px; -} - -.helpText { - margin-left: 144px; -} - -table#observation-input1,table#observation-input2,table#observation-input3,table#observation-input4 { - width: auto; -} - -table#observation-input1 td, table#observation-input1 input, table#observation-input2 td, table#observation-input2 input, table#observation-input3 td, table#observation-input3 input, table#observation-input4 td, table#observation-input4 input{ - margin: 1px; - padding: 1px; -} - -table#observation-input1 input,table#observation-input2 input,table#observation-input3 input,table#observation-input4 input { - width: 50px; - border: solid silver 1px; -} - -table th, table td { - text-align: center; -} - -table .alt-row { - background-color: #e7e7e7; -} - -table tr.table-selected { - border-top: solid blue 1px; - border-bottom: solid blue 1px; -} - -table td.table-selected { - border-left: solid blue 1px; - border-right: solid blue 1px; -} - -table thead.table-header { - display: table-header-group; -} - -table td.first { - border-left: dashed grey 2px; -} - -input.edited[type="text"], input.edited[type="password"], select.edited { - border: solid green 1px; -} - -input.saving[type="text"], input.saving[type="password"], select.saving { - background: url(../../../media/images/ajax-loader.gif) no-repeat right; - font-style: italic; -} - -.ui-widget-header { - font-weight: normal; - padding-left: 3px; - padding-right: 3px; - -} -table.species-grid thead th, table.species-grid tbody td, table.sticky-header thead th { - font-size: 80%; - font-family: Tahoma, Geneva, sans-serif; -} \ No newline at end of file diff --git a/prebuilt_forms/dynamic_sample_occurrence.php b/prebuilt_forms/dynamic_sample_occurrence.php index fbea6115..86e8fdab 100644 --- a/prebuilt_forms/dynamic_sample_occurrence.php +++ b/prebuilt_forms/dynamic_sample_occurrence.php @@ -2248,6 +2248,12 @@ protected static function build_grid_taxon_label_function($args, $options) { '} else {' . "\n" . ' $r = "{taxon}";' . "\n" . '}' . "\n"; + // If the species name available on screen is not the same as the recorded name, + // then inform the user of the recorded name. + $php .= + 'if ("{previously_recorded_as}"!="" && "{previously_recorded_as}"!=NULL) {' . "\n" . + ' $r .= "
Previously recorded as {previously_recorded_as}";' . "\n" . + '}' . "\n"; // This bit optionally adds '- common' or '- latin' depending on what was // being searched. if (isset($args['species_include_both_names']) && $args['species_include_both_names']) { diff --git a/prebuilt_forms/ebms_atlas_map.php b/prebuilt_forms/ebms_atlas_map.php index 2a36fa7d..3328f531 100644 --- a/prebuilt_forms/ebms_atlas_map.php +++ b/prebuilt_forms/ebms_atlas_map.php @@ -280,11 +280,29 @@ protected static function get_form_html($args, $auth, $attributes) { $esFilter = self::createEsFilterHtml('taxon.accepted_taxon_id', self::$externalKey, 'match_phrase', 'must'); // Exclude rejected records in ES queries. $esFilter .= self::createEsFilterHtml('identification.verification_status', 'R', 'term', 'must_not'); + // Only those that pass auto-checks should be included. + $esFilter .= self::createEsFilterHtml('identification.auto_checks.result', 'true', 'term', 'must'); + // Do not include any immature life stages + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'egg', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'Egg', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'larva', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'Larva', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'larvae', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'Larvae', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'pupa', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'Pupa', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'pupae', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'Pupae', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'cocoon', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'Cocoon', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'caterpillar', 'term', 'must_not'); + $esFilter .= self::createEsFilterHtml('occurrence.life_stage', 'Caterpillar', 'term', 'must_not'); } else { self::$taxaTaxonListId = ''; $esFilter = ''; } + if (self::$dataYearFilter != 0) { $esFilter .= self::createEsFilterHtml('event.year', self::$dataYearFilter, 'match_phrase', 'must'); } @@ -302,7 +320,6 @@ protected static function get_form_html($args, $auth, $attributes) { * Empty string as no HTML required. */ protected static function get_control_source($auth, $args, $tabalias, $options) { - dpm($options); return ElasticsearchReportHelper::source($options); } @@ -583,10 +600,6 @@ protected static function getArgDefaults($args) { * Hidden input HTML. */ protected static function createEsFilterHtml($field, $value, $queryType, $boolClause) { - // dpm([ - // 'field' => $field, - // 'value' => $value - // ]); $r = << @@ -621,8 +634,6 @@ protected static function getNames($auth) { self::$externalKey = $species_details[0]['external_key']; } - //dpm($species_details[0]); - //self::$preferred = $species_details[0]['taxon_plain']; self::$preferred = $species_details[0]['taxon']; } } diff --git a/prebuilt_forms/extensions/misc_extensions.php b/prebuilt_forms/extensions/misc_extensions.php index e1559721..534447fa 100644 --- a/prebuilt_forms/extensions/misc_extensions.php +++ b/prebuilt_forms/extensions/misc_extensions.php @@ -520,7 +520,7 @@ public static function set_page_title($auth, $args, $tabalias, $options, $path) * Helper method to enable jQuery tooltips. */ public static function enable_tooltips() { - drupal_add_library('system', 'ui.tooltip', TRUE); + data_entry_helper::add_resource('jquery_ui'); data_entry_helper::$javascript .= " $('form#entry_form').tooltip({ open: function(event, ui) { diff --git a/prebuilt_forms/extensions/taxon_input_extras.php b/prebuilt_forms/extensions/taxon_input_extras.php index f37d18df..a337fa62 100644 --- a/prebuilt_forms/extensions/taxon_input_extras.php +++ b/prebuilt_forms/extensions/taxon_input_extras.php @@ -37,13 +37,11 @@ class extension_taxon_input_extras { * * @title - Override the title of the box displaying the hints. */ public static function add_species_hints($auth, $args, $tabalias, $options, $path) { - // enable nice tooltips - //drupal_add_library('system', 'ui.tooltip', true); $options = array_merge([ 'title' => 'Hints relating to species names entered', ], $options); $filePath = hostsite_get_public_file_path(); - data_entry_helper::$javascript .= "initSpeciesHints('$filePath/indicia/speciesHints.json');\n"; + data_entry_helper::$javascript .= "initSpeciesHints('/$filePath/indicia/speciesHints.json');\n"; return '

' . lang::get($options['title']) . '

' . "
"; } diff --git a/prebuilt_forms/group_edit.php b/prebuilt_forms/group_edit.php index 3f8fa822..a6368a09 100644 --- a/prebuilt_forms/group_edit.php +++ b/prebuilt_forms/group_edit.php @@ -74,6 +74,13 @@ public static function get_parameters() { 'captionField' => 'term', 'extraParams' => ['termlist_external_key' => 'indicia:group_types'], ], + [ + 'name' => 'default_group_label', + 'caption' => 'Default group label', + 'description' => 'What should a group be referred to as? E.g. project, activity etc. This will be overriden with the selected group\'s type when editing.', + 'type' => 'text_input', + 'default' => 'group', + ], [ 'name' => 'parent_group_type', 'caption' => 'Parent group type', @@ -364,6 +371,9 @@ public static function get_form($args, $nid) { $args['filter_types'] = json_decode($args['filter_types'], TRUE); $reloadPath = self::getReloadPath(); data_entry_helper::$website_id = $args['website_id']; + if (!empty($args['default_group_label'])) { + self::$groupType = strtolower($args['default_group_label']); + } // Maintain compatibility with form settings from before group type became // multiselect. if (empty($args['group_type'])) { diff --git a/prebuilt_forms/includes/dynamic.php b/prebuilt_forms/includes/dynamic.php index 9a739bb2..3f0b5998 100644 --- a/prebuilt_forms/includes/dynamic.php +++ b/prebuilt_forms/includes/dynamic.php @@ -265,14 +265,17 @@ public static function get_form($args, $nid) { } // Get authorisation tokens to update and read from the Warehouse. We allow - // child classes to generate this first if subclassed. - if (self::$auth) { - $auth = self::$auth; - } - else { + // child classes to generate this first if subclassed. So, if not already + // generated, or we are on a data entry form without write auth (which can + // happen when loading multiple pages using a view), then fetch read write + // auth. + if (!self::$auth || (empty(self::$auth['write']) && call_user_func([self::$called_class, 'isDataEntryForm']))) { $auth = data_entry_helper::get_read_write_auth($args['website_id'], $args['password']); self::$auth = $auth; } + else { + $auth = self::$auth; + } // Determine how the form was requested and therefore what to output. $mode = (method_exists(self::$called_class, 'getMode')) ? call_user_func([self::$called_class, 'getMode'], $args, $nid) : ''; diff --git a/prebuilt_forms/includes/groups.php b/prebuilt_forms/includes/groups.php index cd02fd6a..405a2616 100644 --- a/prebuilt_forms/includes/groups.php +++ b/prebuilt_forms/includes/groups.php @@ -16,7 +16,6 @@ * 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. * - * @author Indicia Team * @license http://www.gnu.org/licenses/gpl.html GPL 3.0 * @link https://github.com/indicia-team/client_helpers */ @@ -56,47 +55,55 @@ function group_authorise_form($args, $readAuth) { * ID of the group. * @param array $readAuth * Authentication tokens. + * @param bool $checkPage + * Set to false to disable checking that the current page path is an iform + * page linked to the group. */ -function group_authorise_group_id($group_id, $readAuth) { +function group_authorise_group_id($group_id, $readAuth, $checkPage = TRUE) { $gu = []; // Loading data into a recording group. Are they a member or is the page // public? // @todo: consider performance - 2 web services hits required to check permissions. if (hostsite_get_user_field('indicia_user_id')) { - $gu = data_entry_helper::get_population_data(array( + $gu = helper_base::get_population_data([ 'table' => 'groups_user', - 'extraParams' => $readAuth + array( + 'extraParams' => $readAuth + [ 'group_id' => $group_id, 'user_id' => hostsite_get_user_field('indicia_user_id'), 'pending' => 'f', - ), - 'nocache' => true - )); - } - $gp = data_entry_helper::get_population_data(array( - 'table' => 'group_page', - 'extraParams' => $readAuth + array('group_id' => $group_id, 'path' => hostsite_get_current_page_path()) - )); - if (count($gp) === 0) { - hostsite_show_message(lang::get('You are trying to access a page which is not available for this group.'), 'warning', TRUE); - hostsite_goto_page(''); - return FALSE; - } - elseif (count($gu) === 0 && $gp[0]['administrator'] !== NULL) { - // Administrator field is null if the page is fully public. Else if not - // a group member, then throw them out. - hostsite_show_message(lang::get('You are trying to access a page for a group you do not belong to.'), 'warning', TRUE); - hostsite_goto_page(''); - return FALSE; + ], + 'nocache' => TRUE, + ]); } - elseif (isset($gu[0]['administrator']) && isset($gp[0]['administrator'])) { - // Use isn't an administrator, and page is administration - // Note: does not work if using TRUE as bool test, only string 't' - if ($gu[0]['administrator'] != 't' && $gp[0]['administrator'] == 't') { - hostsite_show_message(lang::get('You are trying to open a group page that you do not have permission to access.')); + if ($checkPage) { + $gp = helper_base::get_population_data([ + 'table' => 'group_page', + 'extraParams' => $readAuth + [ + 'group_id' => $group_id, + 'path' => hostsite_get_current_page_path(), + ] + ]); + if (count($gp) === 0) { + hostsite_show_message(lang::get('You are trying to access a page which is not available for this group.'), 'warning', TRUE); hostsite_goto_page(''); return FALSE; } + if (count($gu) === 0 && $gp[0]['administrator'] !== NULL) { + // Administrator field is null if the page is fully public. Else if not + // a group member, then throw them out. + hostsite_show_message(lang::get('You are trying to access a page for a group you do not belong to.'), 'warning', TRUE); + hostsite_goto_page(''); + return FALSE; + } + elseif (isset($gu[0]['administrator']) && isset($gp[0]['administrator'])) { + // Use isn't an administrator, and page is administration + // Note: does not work if using TRUE as bool test, only string 't' + if ($gu[0]['administrator'] != 't' && $gp[0]['administrator'] == 't') { + hostsite_show_message(lang::get('You are trying to open a group page that you do not have permission to access.')); + hostsite_goto_page(''); + return FALSE; + } + } } return count($gu) > 0; } @@ -118,7 +125,7 @@ function group_authorise_group_id($group_id, $readAuth) { * Group field values loaded from the database. */ function group_apply_report_limits(array &$args, $readAuth, $nid, $isMember) { - $group = data_entry_helper::get_population_data([ + $group = helper_base::get_population_data([ 'table' => 'group', 'extraParams' => $readAuth + ['id' => $_GET['group_id'], 'view' => 'detail'] ]); diff --git a/prebuilt_forms/includes/report_filters.php b/prebuilt_forms/includes/report_filters.php index e79e3fa3..df27eb61 100644 --- a/prebuilt_forms/includes/report_filters.php +++ b/prebuilt_forms/includes/report_filters.php @@ -1072,17 +1072,25 @@ public function getTitle() { */ public function getControls() { $r = '
' . lang::get('Please note, your options for quality filtering are restricted by your access permissions in this context.') . '
'; + $r .= data_entry_helper::radio_group([ + 'label' => lang::get('Samples to include or exclude'), + 'fieldname' => 'quality_op', + 'id' => 'quality_op' . (empty($options['standalone']) ? '' : '--standalone'), + 'lookupValues' => [ + 'in' => lang::get('Include'), + 'not in' => lang::get('Exclude'), + ], + 'default' => 'in', + ]); $r .= data_entry_helper::select([ - 'label' => lang::get('Samples to include'), 'fieldname' => 'quality', 'class' => 'quality-filter', 'lookupValues' => [ - 'V' => lang::get('Accepted records only'), - 'P' => lang::get('Not reviewed'), - '!R' => lang::get('Exclude not accepted records'), - '!D' => lang::get('Exclude queried or not accepted records'), + 'P' => lang::get('Pending'), + 'V' => lang::get('Accepted'), + 'R' => lang::get('Not accepted'), + 'D' => lang::get('Queried or rejected'), 'all' => lang::get('All records'), - 'R' => lang::get('Not accepted records only'), ], ]); $r .= data_entry_helper::select([ @@ -1202,6 +1210,12 @@ public function getControls(array $readAuth, array $options) { HTML; + $selectWebsiteFirstInfo = "

$lang[selectWebsiteToLoadSurveys]

"; + } + else { + $website = $websites[0]; + $r .= "
"; + $selectWebsiteFirstInfo = ''; } } else { @@ -1209,7 +1223,8 @@ public function getControls(array $readAuth, array $options) { // is on the website details->milestones tab for a single website) so // supply this as a param, we don't need to worry about sharing as other // websites will have their own milestones. - $r .= ''; + $r .= "
"; + $selectWebsiteFirstInfo = ''; } // Put a cached list of all available surveys in indiciaData to save hits // when building source pane descriptions. @@ -1232,6 +1247,7 @@ public function getControls(array $readAuth, array $options) {

$lang[surveyDatasets]

$surveysFilterInput $surveysOpSelect + $selectWebsiteFirstInfo
@@ -1362,6 +1378,10 @@ function status_filter_control($readAuth, $options) { * verification rule tools are available for the user to apply their own * data checks. Enables options for filtering on the outcome of custom * rule checks. + * * layout - defaults to horizontal, but can be set to vertical so that the + * panel buttons are arranged vertically. + * * initialState - defaults to closed, but can be set to open if you want + * the panel buttons to be always visible. Works well with vertical layout. * @param int $website_id * The current website's warehouse ID. * @param string $hiddenStuff @@ -1418,6 +1438,8 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu 'period_within_year', 'without_polygon', ], + 'layout' => 'horizontal', + 'initialState' => 'closed', ], $options); // Introduce some extra quick filters useful for verifiers. if ($options['sharing'] === 'verification') { @@ -1432,6 +1454,7 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu unset($options['presets']['my-groups']); unset($options['presets']['my-groups-locality']); } + data_entry_helper::$indiciaData['filterEntity'] = $options['entity']; // If in the warehouse we don't need to worry about the iform master list. if (function_exists('hostsite_get_config_value')) { $options = array_merge( @@ -1453,6 +1476,7 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu hostsite_add_library('collapse'); } $lang = [ + 'closeFilterBuilder' => lang::get('Close Filter Builder'), 'context' => lang::get('Context'), 'deleteFilter' => lang::get('Delete filter'), 'filter' => lang::get('Filter'), @@ -1460,6 +1484,7 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu 'filterSaveInstruct' => lang::get('To save these filter settings for future use, give the filter a name then click Save Filter.'), 'filterName' => lang::get('Filter name'), 'saveFilter' => lang::get('Save filter'), + 'saveFilterAs' => lang::get('Save filter as'), 'selectStoredFilter' => lang::get('Select stored filter'), 'storedFilters' => lang::get('Stored filters'), ]; @@ -1634,7 +1659,7 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu } } } - $r = '
'; + $r = "
"; if ($options['allowSave'] && $options['admin']) { if (empty($_GET['filters_user_id'])) { // New filter to create, so sharing type can be edited. @@ -1660,11 +1685,15 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu $r .= data_entry_helper::hidden_text(['fieldname' => 'filter:sharing']); } } + $formInline = $options['layout'] === 'horizontal' ? ' form-inline' : ''; if ($options['allowLoad']) { + // Position of changed indicator asterisk depends on space available due to + // layout. + $changed = $options['layout'] === 'horizontal' ? "*" : ''; $r .= << +
$lang[storedFilters] - + $changed HTML; $r .= '
'; if ($customDefs) { @@ -1728,16 +1757,21 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu 'class' => ' class="' . $indicia_templates['buttonDefaultClass'] . '"', 'caption' => lang::get('Reset'), ]); - $r .= helper_base::apply_static_template('button', [ - 'id' => 'filter-build', - 'title' => lang::get('Create a custom filter'), - 'class' => ' class="' . $indicia_templates['buttonDefaultClass'] . '"', - 'caption' => lang::get('Create a filter'), - ]); + if ($options['initialState'] === 'closed') { + $r .= helper_base::apply_static_template('button', [ + 'id' => 'filter-build', + 'title' => lang::get('Create a custom filter'), + 'class' => ' class="' . $indicia_templates['buttonDefaultClass'] . '"', + 'caption' => lang::get('Create a filter'), + ]); + } $r .= '
'; - $r .= '