diff --git a/ElasticsearchProxyHelper.php b/ElasticsearchProxyHelper.php index 7440552b..928340ee 100644 --- a/ElasticsearchProxyHelper.php +++ b/ElasticsearchProxyHelper.php @@ -17,11 +17,12 @@ * 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 */ +use Firebase\JWT\JWT; + /** * Exception class for request abort. */ @@ -410,10 +411,10 @@ private static function proxyVerifyIds() { } $statuses = $_POST['doc']['identification'] ?? []; return [ - 'updated' => self::internalModifyListOnES( + 'updated' => self::internalModifyListOnEs( $_POST['ids'], $statuses, - isset($_POST['doc']['metadata']['website']['id']) ? $_POST['doc']['metadata']['website']['id'] : NULL + $_POST['doc']['metadata']['website']['id'] ?? NULL ), ]; } @@ -526,7 +527,7 @@ private static function getOccurrenceIdPageFromFilter($nid, $filter, $searchAfte * records after redetermination, until Logstash fills in the taxonomy * again. * - * @param int + * @return int * Number of updated records. */ private static function processWholeEsFilter($nid, array $statuses, $websiteIdToModify = NULL) { @@ -538,7 +539,7 @@ private static function processWholeEsFilter($nid, array $statuses, $websiteIdTo } $ids = self::getOccurrenceIdsFromFilter($nid, $_POST['occurrence:idsFromElasticFilter']); - self::internalModifyListOnES($ids, $statuses, $websiteIdToModify); + self::internalModifyListOnEs($ids, $statuses, $websiteIdToModify); try { self::updateWarehouseVerificationAction($ids, $nid); } @@ -620,7 +621,7 @@ private static function proxyRedetIds() { // Set website ID to 0, basically disabling the ES copy of the record until // a proper update with correct taxonomy information comes through. return [ - 'updated' => self::internalModifyListOnES($_POST['ids'], [], 0), + 'updated' => self::internalModifyListOnEs($_POST['ids'], [], 0), ]; } @@ -697,7 +698,7 @@ private static function getPassThroughUrlParams(array $default, array $params) { * @return int * Number of records updated. */ - private static function internalModifyListOnES(array $ids, array $statuses, $websiteIdToModify) { + private static function internalModifyListOnEs(array $ids, array $statuses, $websiteIdToModify) { $url = self::getEsUrl() . "/_update_by_query"; $scripts = []; if (!empty($statuses['verification_status'])) { @@ -839,7 +840,7 @@ public static function getHttpRequestHeaders($config, $contentType = 'applicatio } $privateKey = file_get_contents($keyFile); $payload = [ - 'iss' => hostsite_get_url('', [], FALSE, TRUE), + 'iss' => rtrim(hostsite_get_url('', [], FALSE, TRUE), '/'), 'http://indicia.org.uk/user:id' => hostsite_get_user_field('indicia_user_id'), 'scope' => $config['es']['scope'], 'exp' => time() + 300, @@ -847,7 +848,7 @@ public static function getHttpRequestHeaders($config, $contentType = 'applicatio $modulePath = \Drupal::service('module_handler')->getModule('iform')->getPath(); // @todo persist the token in the cache? require_once "$modulePath/lib/php-jwt/vendor/autoload.php"; - $token = \Firebase\JWT\JWT::encode($payload, $privateKey, 'RS256'); + $token = JWT::encode($payload, $privateKey, 'RS256'); $headers[] = "Authorization: Bearer $token"; } return $headers; @@ -855,8 +856,18 @@ public static function getHttpRequestHeaders($config, $contentType = 'applicatio /** * A simple wrapper for the cUrl functionality to POST to Elastic. - */ - private static function curlPost($url, $data, $getParams = [], $multipart = FALSE) { + * + * @param string $url + * Warehouse REST API Elasticsearch endpoint URL. + * @param array $data + * ES request object. Can contain a property `proxyCacheTimeout` if the + * response should be cached for a number of seconds. + * @param array $getParams + * Optional query string parameters to add to the URL, e.g. _source. + * @param bool $multipart + * Set to TRUE if doint a multi-part form submission. + */ + public static function curlPost($url, array $data, array $getParams = [], $multipart = FALSE) { $curlResponse = FALSE; $cacheTimeout = FALSE; if (!empty($data['proxyCacheTimeout'])) { @@ -956,7 +967,7 @@ private static function checkPermissionsFilter(array $post, array $readAuth, $ni if (substr($line, 0, 1) === '@') { $parts = explode('=', $line, 2); $decoded = json_decode($parts[1]); - $options[substr($parts[0], 1)] = $decoded === NULL ? $parts[1] : $decoded; + $options[substr($parts[0], 1)] = $decoded ?? $parts[1]; } else { // Finish loop as done permissionFilters control options. @@ -974,7 +985,13 @@ private static function checkPermissionsFilter(array $post, array $readAuth, $ni } } - private static function buildEsQueryFromRequest($post) { + /** + * Convert a posted request to an ES query. + * + * @param array $post + * Posted request data. + */ + private static function buildEsQueryFromRequest(array $post) { $query = array_merge([ 'bool_queries' => [], ], $post); @@ -1046,14 +1063,6 @@ private static function buildEsQueryFromRequest($post) { $queryDef = [$qryConfig['query_type'] => new stdClass()]; } elseif (in_array($qryConfig['query_type'], $fieldValueQueryTypes)) { - // One of the standard ES field based query types (e.g. term or match). - $queryDef = [$qryConfig['query_type'] => [$qryConfig['field'] => $qryConfig['value']]]; - } - elseif (in_array($qryConfig['query_type'], $fieldQueryTypes)) { - // A query type that just needs a field name. - $queryDef = [$qryConfig['query_type'] => ['field' => $qryConfig['field']]]; - } - elseif (in_array($qryConfig['query_type'], $arrayFieldQueryTypes)) { // One of the standard ES field based query types (e.g. term or match). // Special handling needed for metadata and release_status filters. if ($qryConfig['field'] == 'metadata.confidential') { @@ -1070,6 +1079,13 @@ private static function buildEsQueryFromRequest($post) { // Omit the filter and release_status = 'R' is applied by default. self::$releaseStatusFilterApplied = TRUE; } + $queryDef = [$qryConfig['query_type'] => [$qryConfig['field'] => $qryConfig['value']]]; + } + elseif (in_array($qryConfig['query_type'], $fieldQueryTypes)) { + // A query type that just needs a field name. + $queryDef = [$qryConfig['query_type'] => ['field' => $qryConfig['field']]]; + } + elseif (in_array($qryConfig['query_type'], $arrayFieldQueryTypes)) { $queryDef = [$qryConfig['query_type'] => [$qryConfig['field'] => json_decode($qryConfig['value'], TRUE)]]; } elseif (in_array($qryConfig['query_type'], $stringQueryTypes)) { @@ -1100,7 +1116,6 @@ private static function buildEsQueryFromRequest($post) { else { $bool[$qryConfig['bool_clause']][] = $queryDef; } - } unset($query['bool_queries']); // Apply a training mode filter. @@ -1361,10 +1376,12 @@ private static function applyFilterDef(array $readAuth, array $definition, array self::applyUserFiltersOccExternalKey($definition, $bool); self::applyUserFiltersSmpId($definition, $bool); self::applyUserFiltersQuality($definition, $bool); + self::applyUserFiltersCertainty($definition, $bool); self::applyUserFiltersIdentificationDifficulty($definition, $bool); self::applyUserFiltersRuleChecks($definition, $bool); - self::applyUserFiltersAutoCheckRule($definition, $bool); self::applyUserFiltersHasPhotos($definition, $bool); + self::applyUserFiltersLicences($definition, $bool, $readAuth); + self::applyUserFiltersCoordinatePrecision($definition, $bool); self::applyUserFiltersWebsiteList($definition, $bool); self::applyUserFiltersSurveyList($definition, $bool); self::applyUserFiltersImportGuidList($definition, $bool); @@ -1522,7 +1539,7 @@ private static function applyUserFiltersTaxonRankSortOrder(array $definition, ar if (!empty($filter)) { if ($filter['op'] === '=') { $bool['must'][] = [ - 'match' => [ + 'term' => [ 'taxon.taxon_rank_sort_order' => $filter['value'], ], ]; @@ -1557,7 +1574,7 @@ private static function applyFlagFilter($flag, array $definition, array &$bool) // Filter op can be =, >= or <=. if (!empty($filter) && $filter['value'] !== 'all') { $bool['must'][] = [ - 'match' => [ + 'term' => [ "taxon.$flag" => $filter['value'] === 'Y', ], ]; @@ -1570,8 +1587,8 @@ private static function applyFlagFilter($flag, array $definition, array &$bool) * For ES purposes, any location_list filter is modified to a searchArea * filter beforehand. * - * @param string $definition - * WKT for the searchArea in EPSG:4326. + * @param array $definition + * Containing WKT for the searchArea in EPSG:4326. * @param array $bool * Bool clauses that filters can be added to (e.g. $bool['must']). */ @@ -1755,9 +1772,17 @@ private static function applyUserFiltersDate(array $definition, array &$bool) { * Bool clauses that filters can be added to (e.g. $bool['must']). */ private static function applyUserFiltersWho(array $definition, array &$bool) { - if (!empty($definition['my_records']) && $definition['my_records'] === '1') { + if (!empty($definition['my_records']) && ((string) $definition['my_records'] === '1' || (string) $definition['my_records'] === '0')) { + $bool[$definition['my_records'] === '1' ? 'must' : 'must_not'][] = [ + 'term' => ['metadata.created_by_id' => hostsite_get_user_field('indicia_user_id')], + ]; + } + if (!empty($definition['recorder_name']) && !empty(trim($definition['recorder_name']))) { $bool['must'][] = [ - 'match' => ['metadata.created_by_id' => hostsite_get_user_field('indicia_user_id')], + 'query_string' => [ + 'default_field' => 'event.recorded_by', + 'query' => '*' . $definition['recorder_name'] . '*', + ], ]; } } @@ -1853,124 +1878,279 @@ private static function applyUserFiltersOccExternalKey(array $definition, array private static function applyUserFiltersQuality(array $definition, array &$bool) { $filter = self::getDefinitionFilter($definition, ['quality']); if (!empty($filter)) { - switch ($filter['value']) { - case 'V1': - $bool['must'][] = ['match' => ['identification.verification_status' => 'V']]; - $bool['must'][] = ['match' => ['identification.verification_substatus' => 1]]; - break; + $valueList = explode(',', $filter['value']); + $defs = []; + foreach ($valueList as $value) { + switch ($value) { + // Answered query. + case 'A': + $defs[] = [ + 'term' => ['identification.query.keyword' => 'A'], + ]; + break; - case 'V': - $bool['must'][] = ['match' => ['identification.verification_status' => 'V']]; - break; + // Plausible. + case 'C3': + $defs[] = [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.verification_status' => 'C']], + ['term' => ['identification.verification_substatus' => 3]], + ], + ], + ]; + break; - case '-3': - $bool['must'][] = [ - 'bool' => [ - 'should' => [ - [ - 'bool' => [ - 'must' => [ - ['term' => ['identification.verification_status' => 'V']], - ], - ], + // Queried. + case 'D': + $defs[] = [ + 'term' => ['identification.query.keyword' => 'Q'], + ]; + break; + + // Decision by other verifiers. + case 'OV': + $userId = hostsite_get_user_field('indicia_user_id'); + $defs[] = [ + 'query_string' => ['query' => "(NOT identification.verifier.id:$userId AND _exists_:identification.verifier.id)"], + ]; + break; + + case 'P': + $defs[] = [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.verification_status' => 'C']], + ['term' => ['identification.verification_substatus' => 0]], ], - [ - 'bool' => [ - 'must' => [ - ['term' => ['identification.verification_status' => 'C']], - ['term' => ['identification.verification_substatus' => 3]], - ], - ], + 'must_not' => [ + ['exists' => ['field' => 'identification.query']], ], ], - ], - ]; - break; + ]; + break; - case 'C3': - $bool['must'][] = ['match' => ['identification.verification_status' => 'C']]; - $bool['must'][] = ['match' => ['identification.verification_substatus' => 3]]; - break; + // Not accepted. + case 'R': + $defs[] = [ + 'term' => ['identification.verification_status' => 'R'], + ]; + break; - case 'C': - $bool['must'][] = ['match' => ['identification.recorder_certainty' => 'Certain']]; - $bool['must_not'][] = ['match' => ['identification.verification_status' => 'R']]; - break; + case 'R4': + $defs[] = [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.verification_status' => 'R']], + ['term' => ['identification.verification_substatus' => 4]], + ], + ], + ]; + break; - case 'L': - $bool['must'][] = [ - 'terms' => [ - 'identification.recorder_certainty.keyword' => [ - 'Certain', - 'Likely', + case 'R5': + $defs[] = [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.verification_status' => 'R']], + ['term' => ['identification.verification_substatus' => 5]], + ], ], - ], - ]; - $bool['must_not'][] = ['match' => ['identification.verification_status' => 'R']]; - break; + ]; + break; - case 'P': - $bool['must'][] = ['match' => ['identification.verification_status' => 'C']]; - $bool['must'][] = ['match' => ['identification.verification_substatus' => 0]]; - $bool['must_not'][] = ['exists' => ['field' => 'identification.query']]; - break; + // Accepted. + case 'V': + $defs[] = ['term' => ['identification.verification_status' => 'V']]; + break; - case '!R': - $bool['must_not'][] = ['match' => ['identification.verification_status' => 'R']]; - break; + case 'V1': + $defs[] = [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.verification_status' => 'V']], + ['term' => ['identification.verification_substatus' => 1]], + ], + ], + ]; + break; - case '!D': - $bool['must_not'][] = [ - 'match' => ['identification.verification_status' => 'R'], - ]; - $bool['must_not'][] = [ - 'terms' => ['identification.query.keyword' => ['Q', 'A']], - ]; - break; + case 'V2': + $defs[] = [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.verification_status' => 'V']], + ['term' => ['identification.verification_substatus' => 2]], + ], + ], + ]; + break; - case 'D': - $bool['must'][] = ['match' => ['identification.query' => 'Q']]; - break; + // Legacy parameters to support old filters. + // Accepted or plausible. + case '-3': + $defs[] = [ + 'bool' => [ + 'should' => [ + // Verified. + ['term' => ['identification.verification_status' => 'V']], + // Or plausible. + [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.verification_status' => 'C']], + ['term' => ['identification.verification_substatus' => 3]], + ], + ], + ], + ], + ], + ]; + break; - case 'A': - $bool['must'][] = ['match' => ['identification.query' => 'A']]; - break; + // Not queried or rejected. + case '!D': + $defs[] = [ + 'bool' => [ + 'must_not' => [ + ['term' => ['identification.verification_status' => 'R']], + ['terms' => ['identification.query.keyword' => ['Q', 'A']]], + ], + ], + ]; + break; - case 'R': - $bool['must'][] = ['match' => ['identification.verification_status' => 'R']]; - break; + // Not rejected. + case '!R': + $defs[] = [ + 'bool' => [ + 'must_not' => [ + ['term' => ['identification.verification_status' => 'R']], + ], + ], + ]; + break; - case 'R4': - $bool['must'][] = ['match' => ['identification.verification_status' => 'R']]; - $bool['must'][] = ['match' => ['identification.verification_substatus' => 4]]; - break; + // Recorder certain. + case 'C': + $defs[] = [ + 'bool' => [ + 'must' => [ + ['term' => ['identification.recorder_certainty.keyword' => 'Certain']], + ], + 'must_not' => [ + ['term' => ['identification.verification_status' => 'R']], + ], + ], + ]; + break; - case 'DR': // Queried or not accepted. - $bool['must'][] = [ - 'bool' => [ - 'should' => [ - [ - 'bool' => [ - 'must' => [ - ['term' => ['identification.verification_status' => 'R']], + case 'DR': + $defs[] = [ + 'bool' => [ + 'should' => [ + ['term' => ['identification.verification_status' => 'R']], + ['match' => ['identification.query' => 'Q']], + ], + ], + ]; + break; + + // Recorder thinks record identification is likely. + case 'L': + $defs[] = [ + 'bool' => [ + 'must' => [ + [ + 'terms' => [ + 'identification.recorder_certainty.keyword' => [ + 'Certain', + 'Likely', + ], ], ], ], - [ - 'bool' => [ - 'must' => [ - ['match' => ['identification.query' => 'Q']], - ], + 'must_not' => [ + ['term' => ['identification.verification_status' => 'R']], + ], + ], + ]; + break; + + default: + // Nothing to do for 'all'. + } + } + if (!empty($defs)) { + $boolGroup = !empty($filter['op']) && $filter['op'] === 'not in' ? 'must_not' : 'must'; + if (count($defs) === 1) { + // Single filter can be simplified. + $bool[$boolGroup][] = [array_keys($defs[0])[0] => array_values($defs[0])[0]]; + } + else { + // Join multiple filters with OR. + $bool[$boolGroup][] = ['bool' => ['should' => $defs]]; + } + } + } + } + + /** + * Converts an Indicia filter definition certainty filter to an ES query. + * + * @param array $definition + * Definition loaded for the Indicia filter. + * @param array $bool + * Bool clauses that filters can be added to (e.g. $bool['must']). + */ + private static function applyUserFiltersCertainty(array $definition, array &$bool) { + $filter = self::getDefinitionFilter($definition, ['certainty']); + if (!empty($filter)) { + $certaintyCodes = explode(',', $filter['value']); + $certaintyMapping = [ + 'C' => 'Certain', + 'L' => 'Likely', + 'U' => 'Maybe', + ]; + $certaintyTerms = []; + foreach ($certaintyCodes as $code) { + if (array_key_exists($code, $certaintyMapping)) { + $certaintyTerms[] = $certaintyMapping[$code]; + } + } + $boolClauses = []; + if (in_array('NS', $certaintyCodes)) { + // Not stated in the list, needs special handling. + $boolClauses['must_not'] = ['exists' => ['field' => 'identification.recorder_certainty']]; + } + if (count($certaintyTerms)) { + $boolClauses['must'] = ['terms' => ['identification.recorder_certainty.keyword' => $certaintyTerms]]; + } + if (count($boolClauses) === 1) { + $bool[array_keys($boolClauses)[0]][] = array_values($boolClauses)[0]; + } + elseif (count($boolClauses) === 2) { + $bool['must'][] = [ + 'bool' => [ + 'should' => [ + [ + 'bool' => [ + array_keys($boolClauses)[0] => [ + array_values($boolClauses)[0], + ], + ], + ], + [ + 'bool' => [ + array_keys($boolClauses)[1] => [ + array_values($boolClauses)[1], ], ], ], ], - ]; - break; - - default: - // Nothing to do for 'all'. + ], + ]; } } } @@ -2013,7 +2193,12 @@ private static function applyUserFiltersIdentificationDifficulty(array $definiti * Bool clauses that filters can be added to (e.g. $bool['must']). */ private static function applyUserFiltersRuleChecks(array $definition, array &$bool) { - $filter = self::getDefinitionFilter($definition, ['autochecks']); + // Also check for legacy autocheck_rule filters which are now merged with + // autochecks. + $filter = self::getDefinitionFilter($definition, [ + 'autocheck_rule', + 'autochecks', + ]); if (!empty($filter)) { if (in_array($filter['value'], ['P', 'F'])) { // Pass or Fail options are auto-checks from the Data Cleaner module. @@ -2043,28 +2228,16 @@ private static function applyUserFiltersRuleChecks(array $definition, array &$bo ], ]; } + else { + // Other filter values are rule names. + $value = str_replace('_', '', $filter['value']); + $bool['must'][] = [ + 'term' => ['identification.auto_checks.output.rule_type' => $value], + ]; + } } } - /** - * Converts an Indicia filter definition auto checks filter to an ES query. - * - * @param array $definition - * Definition loaded for the Indicia filter. - * @param array $bool - * Bool clauses that filters can be added to (e.g. $bool['must']). - */ - private static function applyUserFiltersAutoCheckRule(array $definition, array &$bool) { - $filter = self::getDefinitionFilter($definition, ['autocheck_rule']); - if (!empty($filter)) { - $value = str_replace('_', '', $filter['value']); - $bool['must'][] = [ - 'term' => ['identification.auto_checks.output.rule_type' => $value], - ]; - } - - } - /** * Converts an Indicia filter definition website_list to an ES query. * @@ -2164,6 +2337,186 @@ private static function applyUserFiltersGroupId(array $definition, array &$bool) } } + /** + * Retrieve licence codes from the database. + * + * Returns a list of licence codes grouped into open and restricted licences. + * + * @param array $readAuth + * Read authenticattion tokens. + * + * @return array + * Array with open and restricted keys, each containing a child array of + * codes. + */ + private static function getLicenceCodes(array $readAuth) { + $r = [ + 'open' => [], + 'restricted' => [], + ]; + $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']; + } + return $r; + } + + /** + * Returns the list of licence codes that should be included in a filter. + * + * @param array $licenceTypes + * List of types that should be in the filter, options are open and + * restricted. + * @param array $licenceCodes + * Licence codes loaded from the database. + * + * @return array + * Simple array of the licence codes that should be included in the filter. + */ + private static function getLicencesToAllow(array $licenceTypes, array $licenceCodes) { + $licencesToAllow = []; + if (in_array('open', $licenceTypes)) { + $licencesToAllow = array_merge($licencesToAllow, $licenceCodes['open']); + } + if (in_array('restricted', $licenceTypes)) { + $licencesToAllow = array_merge($licencesToAllow, $licenceCodes['restricted']); + } + return $licencesToAllow; + } + + /** + * Converts an Indicia filter's licences or media_licences to an ES query. + * + * @param array $definition + * Definition loaded for the Indicia filter. + * @param array $bool + * Bool clauses that filters can be added to (e.g. $bool['must']). + * @param array $readAuth + * Read authentication tokens. + */ + private static function applyUserFiltersLicences(array $definition, array &$bool, array $readAuth) { + $licenceCodes = self::getLicenceCodes($readAuth); + $filter = self::getDefinitionFilter($definition, ['licences']); + if (!empty($filter)) { + $licenceTypes = explode(',', $filter['value']); + if (count(array_diff(['none', 'open', 'restricted'], $licenceTypes)) > 0) { + // Record licences filter. Build a list of possibilities. + $options = []; + if (in_array('none', $licenceTypes)) { + // Add option for records that have no licences. + $options[] = [ + 'bool' => [ + 'must_not' => [ + 'exists' => ['field' => 'metadata.licence_code'], + ], + ], + ]; + } + // Add options for the codes matching the requested licence types. + if (in_array('open', $licenceTypes) || in_array('restricted', $licenceTypes)) { + $options[] = [ + 'terms' => [ + 'metadata.licence_code.keyword' => self::getLicencesToAllow($licenceTypes, $licenceCodes), + ], + ]; + } + $bool['must'][] = [ + 'bool' => ['should' => $options], + ]; + } + } + $filter = self::getDefinitionFilter($definition, ['media_licences']); + if (!empty($filter)) { + $licenceTypes = explode(',', $filter['value']); + if (count(array_diff(['none', 'open', 'restricted'], $licenceTypes)) > 0) { + // Media licences filter. Build a list of possibilities, starting with + // allowing records with no photos. + $options = [ + [ + 'bool' => [ + 'must_not' => [ + 'nested' => [ + 'path' => 'occurrence.media', + 'query' => [ + 'exists' => ['field' => 'occurrence.media'], + ], + ], + ], + ], + ], + ]; + if (in_array('none', $licenceTypes)) { + // Add option for records with photos that have no licences. + $options[] = [ + 'bool' => [ + 'must_not' => [ + 'nested' => [ + 'path' => 'occurrence.media', + 'query' => [ + 'exists' => ['field' => 'occurrence.media.licence'], + ], + ], + ], + ], + ]; + } + // Add options for the codes matching the requested licence types. + if (in_array('open', $licenceTypes) || in_array('restricted', $licenceTypes)) { + $options[] = [ + 'nested' => [ + 'path' => 'occurrence.media', + 'query' => [ + 'terms' => [ + 'occurrence.media.licence.keyword' => self::getLicencesToAllow($licenceTypes, $licenceCodes), + ], + ], + ], + ]; + } + $bool['must'][] = [ + 'bool' => ['should' => $options], + ]; + } + } + } + + /** + * Converts a filter definition coordinate_precision filter to an ES query. + * + * @param array $definition + * Definition loaded for the Indicia filter. + * @param array $bool + * Bool clauses that filters can be added to (e.g. $bool['must']). + */ + 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'] ?? '<='; + if ($filter['op'] === '=') { + $bool['must'][] = [ + 'term' => ['location.coordinate_uncertainty_in_meters' => $filter['value']], + ]; + } + else { + $op = $filter['op'] === '<=' ? 'lte' : 'gt'; + $bool['must'][] = [ + 'range' => [ + 'location.coordinate_uncertainty_in_meters' => [ + $op => $filter['value'], + ], + ], + ]; + } + } + } + /** * Converts an Indicia filter definition has_photos filter to an ES query. * @@ -2343,7 +2696,7 @@ private static function bulkMoveIds($nid, array $ids, $datasetMappings, $prechec if (!$precheck && $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. - self::internalModifyListOnES($ids, [], 0); + self::internalModifyListOnEs($ids, [], 0); } return $response['output']; } diff --git a/ElasticsearchReportHelper.php b/ElasticsearchReportHelper.php index fa56e839..b02ba0af 100644 --- a/ElasticsearchReportHelper.php +++ b/ElasticsearchReportHelper.php @@ -92,6 +92,10 @@ class ElasticsearchReportHelper { 'caption' => 'Submitted on', 'description' => 'Date the record was submitted.', ], + 'metadata.licence_code' => [ + 'caption' => 'Licence', + 'description' => 'Code for the licence that applies to the record.', + ], 'metadata.website.id' => [ 'caption' => 'Website ID', 'description' => 'Unique ID of the website the record was submitted from.', @@ -686,6 +690,7 @@ public static function groupIntegration(array $options) { if (empty($group_id) && $options['missingGroupIdBehaviour'] !== 'showAll') { hostsite_show_message(lang::get('The link you have followed is invalid.'), 'warning', TRUE); hostsite_goto_page(''); + return ''; } require_once 'prebuilt_forms/includes/groups.php'; $member = group_authorise_group_id($group_id, $options['readAuth']); @@ -708,6 +713,7 @@ public static function groupIntegration(array $options) { if (!count($groups)) { hostsite_show_message(lang::get('The link you have followed is invalid.'), 'warning', TRUE); hostsite_goto_page(''); + return ''; } $group = $groups[0]; if ($options['showGroupSummary']) { @@ -1141,7 +1147,7 @@ public static function filterSummary(array $options) { helper_base::$late_javascript .= << TRUE, + ], $options); + return media_filter_control($options['readAuth'], $options); + } + /** * Output a selector for record status. * @@ -1166,9 +1191,9 @@ public static function statusFilters(array $options) { $options = array_merge([ 'sharing' => 'reporting', 'elasticsearch' => TRUE, + 'standalone' => TRUE, ], $options); - - return status_control($options['readAuth'], $options); + return status_filter_control($options['readAuth'], $options); } /** @@ -2418,7 +2443,10 @@ private static function convertValueToFilterList($report, array $params, $output // Build a hidden input which causes filtering to this list. $keys = []; foreach ($listEntries as $row) { - $keys[] = $row[$outputField]; + if ($row[$outputField]) { + // Don't want nulls as they break ES terms filters. + $keys[] = $row[$outputField]; + } } return str_replace('"', '"', json_encode($keys)); } diff --git a/data_entry_helper.php b/data_entry_helper.php index 19dad881..5b703cfa 100644 --- a/data_entry_helper.php +++ b/data_entry_helper.php @@ -5846,7 +5846,6 @@ public static function multiple_places_species_checklist($options) { 'Incorrect configuration - multiple_places_species_checklist requires a @include_sref_handler_jsspatialSystem option.', $indicia_templates['messageBox'] ); - return; } // The ID must be done here so it can be accessed by both the species grid and the buttons. $code = rand(0, 1000); @@ -5864,6 +5863,7 @@ public static function multiple_places_species_checklist($options) { self::includeSrefHandlerJs([$options['spatialSystem'] => '']); $r = ''; if (isset($options['sample_method_id'])) { + require_once 'prebuilt_forms/includes/form_generation.php'; $sampleAttrs = self::getMultiplePlacesSpeciesChecklistSubsampleAttrs($options); foreach ($sampleAttrs as &$attr) { $attr['fieldname'] = "sc:n::$attr[fieldname]"; diff --git a/helper_base.php b/helper_base.php index dc63c799..de6f4e59 100644 --- a/helper_base.php +++ b/helper_base.php @@ -836,6 +836,7 @@ public static function getTranslations(array $strings) { * * brc_charts * * bigr * * d3 + * * html2canvas */ public static function add_resource($resource) { // Ensure indiciaFns is always the first resource added. @@ -1280,6 +1281,27 @@ public static function get_resources() { 'https://unpkg.com/brc-atlas-bigr@2.4.0/dist/bigr.min.umd.js', ], ], + 'html2canvas' => [ + 'javascript' => [ + 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', + ], + ], + 'brc_atlas_e' => [ + 'deps' => [ + 'd3_v7', + ], + 'stylesheets' => [ + 'https://cdn.jsdelivr.net/gh/biologicalrecordscentre/brc-atlas@1.1.6/dist/brcatlas_e.umd.css', + ], + 'javascript' => [ + 'https://cdn.jsdelivr.net/gh/biologicalrecordscentre/brc-atlas@1.1.6/dist/brcatlas_e.umd.min.js', + ], + ], + 'd3_v7' => [ + 'javascript' => [ + 'https://d3js.org/d3.v7.min.js', + ], + ], ]; } return self::$resource_list; @@ -2367,6 +2389,7 @@ public static function getIndiciaData() { 'buttonDefaultClass' => $indicia_templates['buttonDefaultClass'], 'buttonHighlightedClass' => $indicia_templates['buttonHighlightedClass'], 'buttonSmallClass' => 'btn-xs', + 'jQueryValidateErrorClass' => $indicia_templates['error_class'], ]; self::$indiciaData['formControlClass'] = $indicia_templates['formControlClass']; self::$indiciaData['inlineErrorClass'] = $indicia_templates['error_class']; @@ -3271,12 +3294,6 @@ private static function apply_error_template($error, $fieldname) { * * **report** - Path to the report file to use when loading data from a * report, e.g. "library/occurrences/explore_list", excluding the .xml * extension. - * * **orderby** - Optional. For a non-default sort order, provide the - * field name to sort by. Can be comma separated to sort by several - * fields in descending order of precedence. - * * **sortdir** - Optional. Specify ASC or DESC to define ascending or - * descending sort order respectively. Can be comma separated if several - * sort fields are specified in the orderby parameter. * * **extraParams** - Array of extra URL parameters to send with the web * service request. Should include key value pairs for the field filters * (for table data) or report parameters (for the report data) as well as diff --git a/import_helper_2.php b/import_helper_2.php index 388dc89a..d1689ad9 100644 --- a/import_helper_2.php +++ b/import_helper_2.php @@ -128,6 +128,7 @@ public static function importer($options) { 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) { @@ -779,10 +780,13 @@ private static function mappingsForm(array $options) { $lang = [ 'columnInImportFile' => lang::get('Column in import file'), 'destinationDatabaseField' => lang::get('Destination database field'), + 'display' => lang::get('Display'), 'instructions' => lang::get($options['mappingsFormIntro']), 'next' => lang::get('Next step'), 'requiredFields' => lang::get('Required fields'), 'requiredFieldsInstructions' => lang::get($options['requiredFieldsIntro']), + 'standardFieldsOnly' => lang::get('standard fields'), + 'standardAndAdvancedFields' => lang::get('standard and advanced fields'), 'title' => lang::get('Map import columns to destination database fields'), ]; self::addLanguageStringsToJs('import_helper_2', [ @@ -843,6 +847,11 @@ private static function mappingsForm(array $options) { return <<$lang[title]

$lang[instructions]

+
+ $lang[display] + + +
@@ -908,42 +917,74 @@ private static function getAvailableDbFieldsAsOptions(array $options, array $ava $shortGroupLabels[$optGroup] = lang::get("optionGroup-$fieldParts[0]-shortLabel"); } // Find variants of field names for auto matching. + $alts = []; switch ($field) { + case 'occurrence:comment': + $alts = ['comment', 'comments', 'notes']; + break; + + case 'occurrence:external_key': + $alts = ['recordkey', 'ref', 'referenceno', 'referencenumber']; + break; + + case 'occurrence:fk_taxa_taxon_list': + $alts = ['commonname', 'scientificname', 'species', 'speciesname', 'taxon', 'taxonname', 'vernacular']; + break; + + case 'occurrence:fk_taxa_taxon_list:search_code': + $alts = ['searchcode','tvk','taxonversionkey']; + break; + case 'sample:date': - $alt = ' data-alt="eventdate"'; + $alts = ['eventdate']; break; case 'sample:entered_sref': - $alt = ' data-alt="gridref,gridreference,spatialref,spatialreference,mapref,mapreference"'; + $alts = ['gridref', 'gridreference', 'spatialref', 'spatialreference', 'mapref', 'mapreference', 'coords', 'coordinates']; break; case 'sample:location_name': - $alt = ' data-alt="location,site,sitename"'; + $alts = ['location', 'site', 'sitename']; break; case 'sample:recorder_names': - $alt = ' data-alt="recorder,recordername,recordernames"'; - break; - - case 'occurrence:fk_taxa_taxon_list': - $alt = ' data-alt="species,speciesname,taxon,taxonname,scientificname"'; + $alts = ['recorder', 'recordername', 'recordernames']; break; default: $alt = ''; } - // Also use caption to pick up custom attributes. - if (substr($field, 3, 5) === 'Attr:') { - switch (preg_replace('/[^a-z]/', '', strtolower($caption))) { - case 'recorder': - case 'recordername': - case 'recordernames': - $alt = ' data-alt="recorder,recordername,recordernames"'; - break; + // Matching variations for some potential custom attribute captions. + if (preg_match('/(.+) \(.+\)/', $caption, $matches)) { + // Strip anything in brackets from caption we are checking. + $captionSimplified = preg_replace('/[^a-z]/', '', strtolower($matches[1])); + } + else { + $captionSimplified = preg_replace('/[^a-z]/', '', strtolower($caption)); + } + $customAttrVariations = [ + ['abundance', 'count', 'qty', 'quantity'], + ['vc', 'vicecounty', 'vicecountynumber'], + ['recorder', 'recordername', 'recordernames', 'recorders'], + ['determinedby', 'determiner', 'identifiedby', 'identifier'], + ['lifestage','stage'], + ]; + foreach ($customAttrVariations as $variationSet) { + if (in_array(strtolower($captionSimplified), $variationSet)) { + unset($variationSet[array_search($captionSimplified, $variationSet)]); + $alts = array_merge($alts, $variationSet); } } - $translatedCaption = lang::get($caption); - $colsByGroup[$optGroup][$translatedCaption] = ""; + // Build the data attribute. + $alt = empty($alts) ? '' : ' data-alt="' . implode(',', $alts) . '"'; + // Translation can be a precise term keyed by the field name, or a loose + // term keyed off the caption. + $translatedCaption = lang::get($field); + if ($translatedCaption === $field) { + $translatedCaption = lang::get($caption); + } + $advanced = in_array($field, $options['advancedFields']) ? ' class="advanced" ' : ''; + $colsByGroup[$optGroup][$translatedCaption] = ""; } $optGroupHtmlList = [""]; foreach ($colsByGroup as $thisColOptionGroup => $optionsList) { @@ -1589,6 +1630,7 @@ private static function clearTemplateCache(array $options) { 'extraParams' => $options['readAuth'] + [ 'entity' => $options['entity'], 'created_by_id' => hostsite_get_user_field('indicia_user_id'), + 'orderby' => 'title', ], ]); } diff --git a/lang/default.php b/lang/default.php index 8f5a1de6..6f3ab1b0 100644 --- a/lang/default.php +++ b/lang/default.php @@ -63,7 +63,8 @@ // Default labels for various database fields. 'occurrence:taxa_taxon_list_id' => 'Species', 'sample:date' => 'Date', - 'sample:entered_sref' => 'Spatial Reference', + 'sample:entered_sref' => 'Spatial reference', + 'sample:entered_sref_system' => 'Spatial reference system', // Spatial reference systems. 'sref:OSGB' => 'British National Grid', @@ -98,6 +99,13 @@ 'Click to Filter Occ_id' => 'Select records by record ID', 'Click to Filter Quality' => 'Select records based on quality criteria such as verification status or presence of photos', 'Click to Filter Source' => 'Select records based on source website, survey or input form', + + // Data cleaner rules. + 'ancillary_species failed' => 'Rarity check failed', + 'identification_difficulty failed' => 'ID difficulty check failed', + 'period failed' => 'Year range check failed', + 'period_within_year failed' => 'Date range check failed', + 'without_polygon failed' => 'Distribution check failed', ]; // Some bigger bits of text better handled with HEREDOC. diff --git a/lang/import_helper_2.php b/lang/import_helper_2.php index 6e8c9681..19e0279f 100644 --- a/lang/import_helper_2.php +++ b/lang/import_helper_2.php @@ -31,7 +31,13 @@ TXT; $default_terms['import2mappingsFormIntro'] = <<
+By default this page shows all the “standard” attributes that will be sufficient for typical +biological record formats. If needed you can add “advanced” attributes to the list - these provide +additional options but are not always straightforward to use. + TXT; $default_terms['import2lookupMatchingFormIntro'] = << 'Basic 1 - species, date, place, survey and comment', 'category' => 'Training/Testing forms', 'description' => 'A very simple form designed to illustrate the prebuilt form development and setup process.' - ); + ]; } /** * Get the list of parameters for this form. - * @return array List of parameters that this form requires. + * + * @return array + * List of parameters that this form requires. */ public static function get_parameters() { - return array( - array( + return [ + [ 'fieldname' => 'species_ctrl', 'label' => 'Species Control Type', 'helpText' => 'The type of control that will be available to select a species.', 'type' => 'select', - 'lookupValues' => array( + 'lookupValues' => [ 'autocomplete' => 'Autocomplete', 'select' => 'Select', 'listbox' => 'List box', 'radio_group' => 'Radio group', - 'species_checklist' => 'Checkbox grid' - ) - ), - array( + 'species_checklist' => 'Checkbox grid', + ], + ], + [ 'fieldname' => 'list_id', 'label' => 'Species List', 'helpText' => 'The species list that species can be selected from.', 'type' => 'select', 'table' => 'taxon_list', 'valueField' => 'id', - 'captionField' => 'title' - ), - array( + 'captionField' => 'title', + ], + [ 'fieldname' => 'preferred', 'label' => 'Preferred species only?', 'helpText' => 'Should the selection of species be limited to preferred names only?', 'type' => 'boolean', - 'required'=>false - ), - array( + 'required' => FALSE, + ], + [ 'fieldname' => 'tabs', 'label' => 'Use Tabbed Interface', 'helpText' => 'If checked, then the page will be built using a tabbed interface style.', 'type' => 'boolean', - 'required'=>false - ) - ); + 'required' => FALSE, + ], + ]; } /** * Return the generated form output. - * @return Form HTML. + * + * @return string + * HTML. */ public static function get_form($args) { $r = "\n"; diff --git a/prebuilt_forms/css/ebms_atlas_map.css b/prebuilt_forms/css/ebms_atlas_map.css new file mode 100644 index 00000000..9e636657 --- /dev/null +++ b/prebuilt_forms/css/ebms_atlas_map.css @@ -0,0 +1,68 @@ +.clabel { + width: 8em; + display: inline-block; + text-align: right; + padding-right: 0.2em; + font-size: 1em; + margin-bottom: 0.5em; +} + +#playPause { + float: left; + width: 30px; + height: 30px; + border: 0px; + background-color: grey; + text-align: center; + vertical-align: middle; + line-height: 30px; + font-weight: bold; + color: white; + cursor: pointer; +} + +.slidecontainer { + float: left; + width: calc(100% - 32px); /* Width of the outside container */ +} + + +/* The slider itself */ +.slider { + margin: 2px !important; + position: relative !important; + -webkit-appearance: none; /* Override default CSS styles */ + appearance: none; + width: 100%; /* Full-width */ + height: 25px; /* Specified height */ + background: #d3d3d3; /* Grey background */ + outline: none; /* Remove outline */ + opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */ + -webkit-transition: .2s; /* 0.2 seconds transition on hover */ + transition: opacity .2s; +} +.slider::before { + position: relative !important; +} + +/* Mouse-over effects */ +.slider:hover { + opacity: 1; /* Fully shown on mouse-over */ +} + +/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */ +.slider::-webkit-slider-thumb { + -webkit-appearance: none; /* Override default look */ + appearance: none; + width: 25px; /* Set a specific slider handle width */ + height: 25px; /* Slider handle height */ + background: blue; /* Green background */ + cursor: pointer; /* Cursor on hover */ +} + +.slider::-moz-range-thumb { + width: 25px; /* Set a specific slider handle width */ + height: 25px; /* Slider handle height */ + background: blue; /* Green background */ + cursor: pointer; /* Cursor on hover */ +} \ No newline at end of file diff --git a/prebuilt_forms/custom_verification_rules_upload.php b/prebuilt_forms/custom_verification_rules_upload.php index ee34778e..18aa0e77 100644 --- a/prebuilt_forms/custom_verification_rules_upload.php +++ b/prebuilt_forms/custom_verification_rules_upload.php @@ -156,9 +156,6 @@ public static function get_form($args, $nid, $response = NULL) { helper_base::$indiciaData['importerDropArea'] = '.dm-uploader'; helper_base::$indiciaData['uploadFileUrl'] = hostsite_get_url('iform/ajax/custom_verification_rules_upload') . "/upload_interim_file/$nid"; helper_base::$indiciaData['uploadRulesStepUrl'] = hostsite_get_url('iform/ajax/custom_verification_rules_upload') . "/upload_rules_step/$nid"; - helper_base::$indiciaData['templates'] = [ - 'warningBox' => $indicia_templates['warningBox'], - ]; return $r; } diff --git a/prebuilt_forms/dynamic_elasticsearch.php b/prebuilt_forms/dynamic_elasticsearch.php index 6ad91f6e..e23e4ee9 100644 --- a/prebuilt_forms/dynamic_elasticsearch.php +++ b/prebuilt_forms/dynamic_elasticsearch.php @@ -320,9 +320,22 @@ protected static function get_control_filterSummary($auth, $args, $tabalias, $op } /** - * Output a selector for a general record access contexts based on permission filters and group permissions etc + * Output a selector for records with or without media. + * + * @link https://indicia-docs.readthedocs.io/en/latest/site-building/iform/helpers/elasticsearch-report-helper.html#elasticsearchreporthelper-mediaFilter + */ + protected static function get_control_mediaFilter($auth, $args, $tabalias, $options) { + return ElasticsearchReportHelper::mediaFilter(array_merge($options, [ + 'readAuth' => $auth['read'], + ])); + } + + /** * Output a selector for record status. * + * Output a selector for a general record access contexts based on permission + * filters and group permissions etc. + * * @link https://indicia-docs.readthedocs.io/en/latest/site-building/iform/helpers/elasticsearch-report-helper.html#elasticsearchreporthelper-statusFilters */ protected static function get_control_statusFilters($auth, $args, $tabalias, $options) { diff --git a/prebuilt_forms/ebms_atlas_map.php b/prebuilt_forms/ebms_atlas_map.php new file mode 100644 index 00000000..2a36fa7d --- /dev/null +++ b/prebuilt_forms/ebms_atlas_map.php @@ -0,0 +1,629 @@ + 'eBMS atlas maps', + 'category' => 'Utilities', + 'description' => 'eBMS atlas map.', + 'recommended' => TRUE, + ); + } + + /** + * Return an array of parameters for the edit tab. + * @return array The parameters for the form. + */ + public static function get_parameters() { + $retVal = array_merge( + array( + //Allows the user to define how the page will be displayed. + array( + 'name' => 'structure', + 'caption' => 'Form Structure', + 'description' => 'Define the structure of the form. Each component must be placed on a new line.
'. + "The following types of component can be specified.
". + "[control name] indicates a predefined control is to be added to the form with the following predefined controls available:
". + "  [tracemap] - displays a tracemap showing seasonal distribution with date slider control
". + "  [year] - allows a year filter to be applied to the data.
". + "  [species] - adds a species selection control
". + "=tab/page name= is used to specify the name of a tab or wizard page (alpha-numeric characters only). ". + "If the page interface type is set to one page, then each tab/page name is displayed as a seperate section on the page. ". + "Note that in one page mode, the tab/page names are not displayed on the screen.
". + "| is used to split a tab/page/section into two columns, place a [control name] on the previous line and following line to split.
", + 'type' => 'textarea', + 'default' => ' +=General= +[species] +[tracemap] +[year]', + 'group' => 'User Interface' + ), + // Initial species. + [ + 'name' => 'taxa_taxon_list_id', + 'caption' => 'Taxa taxon list ID', + 'description' => 'Taxa taxon list ID to initialise the visualisation.', + 'type' => 'string', + 'required' => FALSE, + 'default' => '', + 'group' => 'Initialise species', + ], + [ + 'name' => 'external_key', + 'caption' => 'External key', + 'description' => 'External key to initialise the visualisation.', + 'type' => 'string', + 'required' => FALSE, + 'default' => '', + 'group' => 'Initialise species', + ], + // Taxon selector. + [ + 'fieldname' => 'list_id', + 'label' => 'Species List', + 'helpText' => 'The species list that species can be selected from.', + 'type' => 'select', + 'table' => 'taxon_list', + 'valueField' => 'id', + 'captionField' => 'title', + 'required' => FALSE, + 'group' => 'Species selection', + 'siteSpecific' => TRUE, + ], + [ + 'name' => 'species_include_authorities', + 'caption' => 'Include species authors in the search string', + 'description' => 'Should species authors be shown in the search + results when searching for a species name?', + 'type' => 'boolean', + 'required' => FALSE, + 'group' => 'Species selection', + ], + [ + 'name' => 'species_include_both_names', + 'caption' => 'Include both names in species controls and added rows', + 'description' => 'When using a species grid with the ability to add + new rows, the autocomplete control by default shows just the + searched taxon name in the drop down. Set this to include both the + latin and common names, with the searched one first. This also + controls the label when adding a new taxon row into the grid.', + 'type' => 'boolean', + 'required' => FALSE, + 'group' => 'Species selection', + ], + // Other stuff + [ + 'name' => 'use_get', + 'caption' => 'Enable setting parameters on URL', + 'description' => 'Check this box if species/year to be set in URL.', + 'type' => 'boolean', + 'required' => FALSE, + 'group' => 'Other', + ], + [ + 'name' => 'geohash_level', + 'caption' => 'Indicate the geohash level - must be a number between 1 and 6. ', + 'description' => 'Indicates the level at which data will be gridded smaller ' . + 'numbers indicate bigger areas. If any other number is specified, the default of 4 is used.', + 'type' => 'int', + 'default' => 4, + 'required' => TRUE, + 'group' => 'Other', + ], + ) + ); + return $retVal; + } + + /** + * Main build function to return the page HTML. + * + * @param array $args + * Page parameters. + * @param int $nid + * Node ID. + * + * @return string + * Page HTML. + */ + public static function get_form($args, $nid) { + + iform_load_helpers(['ElasticsearchReportHelper']); + ElasticsearchReportHelper::enableElasticsearchProxy($nid); + //$enabled = ElasticsearchReportHelper::enableElasticsearchProxy($nid); // Available in the newer BaseDynamicDetails base class - not yet on eBMS + + $enabled = TRUE; + if ($enabled) { + return parent::get_form($args, $nid); + } + global $indicia_templates; + return str_replace('{message}', lang::get('This page cannot be accessed due to the server being unavailable.'), $indicia_templates['warningBox']); + } + + /** + * Override the get_form_html function. + * getForm in dynamic.php will now call this. + * Vary the display of the page based on the interface type + * + * @package Client + * @subpackage PrebuiltForms + */ + protected static function get_form_html($args, $auth, $attributes) { + + // First set variables from parameters if set + $getForm = isset($args['use_get']) ? $args['use_get'] : FALSE; + + if ($getForm) { + empty($_GET['external_key']) ? self::$externalKey='' : self::$externalKey=$_GET['external_key']; + empty($_GET['taxa_taxon_list_id']) ? self::$taxaTaxonListId='' : self::$taxaTaxonListId=$_GET['taxa_taxon_list_id']; + empty($_GET['data_year_filter']) ? self::$dataYearFilter=0 : self::$dataYearFilter=$_GET['data_year_filter']; + } else { + empty($_POST['external_key']) ? self::$externalKey='' : self::$externalKey=$_POST['external_key']; + empty($_POST['taxa_taxon_list_id']) ? self::$taxaTaxonListId='' : self::$taxaTaxonListId=$_POST['taxa_taxon_list_id']; + empty($_POST['data_year_filter']) ? self::$dataYearFilter=0 : self::$dataYearFilter=$_POST['data_year_filter']; + } + // Otherwise get from form fields if set + if (self::$externalKey == '' && isset($args['external_key'])) { + self::$externalKey = $args['external_key']; + } + if (self::$taxaTaxonListId == '' && isset($args['taxa_taxon_list_id'])) { + self::$taxaTaxonListId = $args['taxa_taxon_list_id']; + } + + self::getNames($auth); + + // In Drupal 9, markup cannot be used in page title, so remove em tags." + // $repArray = ['', '']; + // if (self::$preferred <> '') { + // $preferredName = lang::get('Seasonal occurrence for {1}', str_replace($repArray, '', self::$preferred)); + // } else { + // $preferredName = lang::get('Seasonal occurrence'); + // } + + // hostsite_set_page_title($preferredName); + // hostsite_set_page_title not working for some reason - set as hidden filed for JS to do instead. + // $hidden = ''; + // Make the external-key available to JS. + + $hidden = ''; + + $getOrPost = $getForm ? 'GET' : 'POST'; + $form = ''; + // The species_autocomplete control returns a taxa_taxon_list_id. Create a hidden input + // on the GET form (URL parameter) that javascript will populate with the selected value + // but initialise it with current value if. + $form .= ''; + $form .= ''; + $form .= ''; + + if (self::$externalKey <> '') { + // Add any general ES taxon filters for taxon specified in URL. We use the ES field taxon.accepted_taxon_id + // which is the external_key. We can't use the *preferred* taxa_taxon_list_id as this is not available on the + // ES index. + $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'); + } else { + self::$taxaTaxonListId = ''; + $esFilter = ''; + } + + if (self::$dataYearFilter != 0) { + $esFilter .= self::createEsFilterHtml('event.year', self::$dataYearFilter, 'match_phrase', 'must'); + } + + return $hidden . $form . $esFilter . parent::get_form_html($args, $auth, $attributes); + } + + + /** + * Initialises the JavaScript required for an Elasticsearch data source. + * + * @link https://indicia-docs.readthedocs.io/en/latest/site-building/iform/helpers/elasticsearch-report-helper.html#elasticsearchreporthelper-source + * + * @return string + * Empty string as no HTML required. + */ + protected static function get_control_source($auth, $args, $tabalias, $options) { + dpm($options); + return ElasticsearchReportHelper::source($options); + } + + /** + * Returns a control to display a trace map. + * + * @global type $indicia_templates + * @param array $auth + * Read authorisation tokens. + * @param array $args + * Form configuration. + * @param array $tabAlias + * @param array $options + * Additional options for the control, e.g. those configured in the form + * structure. + * + * @return string + * HTML for the control. + */ + + protected static function get_control_tracemap($auth, $args, $tabalias, $options) { + + $glevel = isset($args['geohash_level']) ? $args['geohash_level'] : 4; + if ($glevel < 1 || $glevel > 6) { + $glevel = 4; + } + + $options = array_merge([ + 'dotcolour' => 'magenta', + 'initplay' => 'no' + ], $options); + + // Hidden controls to store map parameters + $hidden = ''; + $hidden .= ''; + + // Loact brc_atlas_e library resource + data_entry_helper::add_resource('brc_atlas_e'); + + if (self::$externalKey <> '') { + + // $optionsSource = [ + // 'extraParams' => $options['extraParams'], + // 'nid' => $options['nid'], + // 'id' => 'tracemapSource', + // 'filterPath' => 'hits.total,hits.hits._source.event.week, hits.hits._source.event.year, hits.hits._source.location.point', + // 'size' => 10000, + // 'mode' => 'docs', + // ]; + + // ElasticsearchReportHelper::source($optionsSource); + // $optionsCustomScript = [ + // 'extraParams' => $options['extraParams'], + // 'nid' => $options['nid'], + // 'source' => 'tracemapSource', + // 'functionName' => 'processTraceMapData', + // ]; + + // Set cache for query + if (self::$dataYearFilter == date("Y") || self::$dataYearFilter == 0) { + $cacheTimeout = 86400; // Set cache for one day + } else { + $cacheTimeout = 86400 * 7; + } + + $optionsSource = [ + 'extraParams' => $options['extraParams'], + 'nid' => $options['nid'], + 'id' => 'tracemapSource', + 'size' => 0, + 'proxyCacheTimeout' => $cacheTimeout, + 'aggregation' => [ + 'aggs' => [ + 'terms' => [ + 'field' => 'event.week', + 'size' => '53' + ], + 'aggs' => [ + 'geohash' => [ + 'geohash_grid' => [ + 'field' => 'location.point', + 'precision' => $glevel + ], + // Getting lat/lon from ES aggregation doesn't seem + // to give centroid of geohash as espected, so instead + // we use one worked out directly from geohash. + // 'aggs' => [ + // 'centroid' => [ + // 'geo_centroid' => [ + // 'field' => 'location.point' + // ] + // ] + // ] + ] + ] + ] + ] + ]; + + ElasticsearchReportHelper::source($optionsSource); + $optionsCustomScript = [ + 'extraParams' => $options['extraParams'], + 'nid' => $options['nid'], + 'source' => 'tracemapSource', + 'functionName' => 'processTraceMapData2', + ]; + + $r = ElasticsearchReportHelper::customScript($optionsCustomScript); + } else { + $r = ''; + } + $r .= '
'; + $r .= '
'; + $r .= '
>
'; + $r .= '
'; + $r .= ''; + $r .= '
'; + $r .= '
'; + $r .= '
'; + $r .= $hidden; + + return $r; + } + + /** + * Returns a control for picking a year or using data from all years. + * + * @global type $indicia_templates + * @param array $auth + * Read authorisation tokens. + * @param array $args + * Form configuration. + * @param array $tabAlias + * @param array $options + * Additional options for the control, e.g. those configured in the form + * structure. + * + * @return string + * HTML for the control. + */ + protected static function get_control_year($auth, $args, $tabAlias, $options) { + + $options = array_merge([ + 'minyear' => 2015, + ], $options); + + $currentYear = date("Y"); + + if (self::$dataYearFilter == 0) { + $checked = 'checked'; + $initYear = ''; + $disabled = 'disabled'; + } else { + $checked = ''; + $initYear = self::$dataYearFilter; + $disabled = ''; + } + + // Dynamically generate the species selection control required. + $r = '
'; + + $r .= ''; + $r .= ''; + $r .= ''; + + $r .= ''; + + $r .= '
'; + + return $r; + } + + + /** + * Returns an h2 title based on the name of the selected taxon. + * + * @global type $indicia_templates + * @param array $auth + * Read authorisation tokens. + * @param array $args + * Form configuration. + * @param array $tabAlias + * @param array $options + * Additional options for the control, e.g. those configured in the form + * structure. + * + * @return string + * HTML for the control. + */ + protected static function get_control_title($auth, $args, $tabAlias, $options) { + + if (self::$preferred <> '') { + $year = self::$dataYearFilter == 0 ? 'all years' : self::$dataYearFilter; + $r = '

' . self::$preferred . ' - ' . $year . '

'; + } else { + $r = ''; + } + return $r; + } + + /** + * Returns a control for picking a species. + * + * @global type $indicia_templates + * @param array $auth + * Read authorisation tokens. + * @param array $args + * Form configuration. + * @param array $extraParams + * Extra parameters pre-configured with taxon and taxon name type filters. + * @param array $options + * Additional options for the control, e.g. those configured in the form + * structure. + * + * @return string + * HTML for the control. + */ + protected static function get_control_species($auth, $args, $tabAlias, $options) { + + $extraParams = $auth['read']; + if ($args['list_id'] !== '') { + $extraParams['taxon_list_id'] = $args['list_id']; + } + $species_ctrl_opts = array_merge([ + 'fieldname' => 'occurrence:taxa_taxon_list_id', + ], $options); + if (isset($species_ctrl_opts['extraParams'])) { + $species_ctrl_opts['extraParams'] = array_merge($extraParams, $species_ctrl_opts['extraParams']); + } + else { + $species_ctrl_opts['extraParams'] = $extraParams; + } + $species_ctrl_opts['extraParams'] = array_merge([ + 'orderby' => 'taxonomic_sort_order', + 'sortdir' => 'ASC', + ], $species_ctrl_opts['extraParams']); + + // Apply options and build species autocomplete formatting function + $opts = [ + 'speciesIncludeAuthorities' => isset($args['species_include_authorities']) ? + $args['species_include_authorities'] : FALSE, + 'speciesIncludeBothNames' => $args['species_include_both_names'], + ]; + data_entry_helper::build_species_autocomplete_item_function($opts); + + // Dynamically generate the species selection control required. + $r = '
'; + $r .= ''; + $r .= '
'; + $r .= call_user_func(['data_entry_helper', 'species_autocomplete'], $species_ctrl_opts); + $r .= '
'; + $r .= '
'; + + return $r; + } + + /** + * When a form version is upgraded introducing new parameters, old forms will not get the defaults for the + * parameters unless the Edit and Save button is clicked. So, apply some defaults to keep those old forms + * working. + */ + protected static function getArgDefaults($args) { + if (!isset($args['interface']) || empty($args['interface'])) + $args['interface'] = 'one_page'; + + if (!isset($args['structure']) || empty($args['structure'])) { + $args['structure'] = +'=General= +[tracemap]'; + } + return $args; + } + + /** + * Make a hidden input specifing a page-wide filter for ES queries. + * + * @return string + * Hidden input HTML. + */ + protected static function createEsFilterHtml($field, $value, $queryType, $boolClause) { + // dpm([ + // 'field' => $field, + // 'value' => $value + // ]); + $r = << + +HTML; + return $r; + } + + /** + * Obtains details of all names for this species from the database. + */ + protected static function getNames($auth) { + + if (self::$taxaTaxonListId <> '' || self::$externalKey <> '') { + iform_load_helpers(['report_helper']); + $extraParams = ['sharing' => 'reporting']; + $extraParams['preferred'] = TRUE; + if (self::$taxaTaxonListId <> '') { + $extraParams['taxa_taxon_list_id'] = self::$taxaTaxonListId; + } + if (self::$externalKey <> '') { + $extraParams['external_key'] = self::$externalKey; + } + $species_details = report_helper::get_report_data([ + 'readAuth' => $auth['read'], + 'dataSource' => 'projects/ebms/ebms_taxon_names', + 'useCache' => FALSE, + 'extraParams' => $extraParams, + ]); + + if (self::$externalKey == '') { + self::$externalKey = $species_details[0]['external_key']; + } + + //dpm($species_details[0]); + //self::$preferred = $species_details[0]['taxon_plain']; + self::$preferred = $species_details[0]['taxon']; + } + } +} \ No newline at end of file diff --git a/prebuilt_forms/extensions/pantheon.php b/prebuilt_forms/extensions/pantheon.php index 839a9934..94263baa 100644 --- a/prebuilt_forms/extensions/pantheon.php +++ b/prebuilt_forms/extensions/pantheon.php @@ -256,4 +256,14 @@ public static function quick_analysis_scratchpad_group($auth, $args, $tabalias, HTML; } + /** + * Adds write tokens to the page so that JS can trigger data services calls. + */ + public static function enable_write_auth($auth, $args, $tabalias, $options, $path) { + $conn = iform_get_connection_details(); + $auth = helper_base::get_read_write_auth($conn['website_id'], $conn['password']); + helper_base::$indiciaData['writeTokens'] = $auth['write_tokens']; + return ''; + } + } diff --git a/prebuilt_forms/importer_2.php b/prebuilt_forms/importer_2.php index be770f4a..eb00c8cd 100644 --- a/prebuilt_forms/importer_2.php +++ b/prebuilt_forms/importer_2.php @@ -90,6 +90,48 @@ public static function get_parameters() { every imported record.

You can use the following replacement tokens in the values: {user_id}, {username}, {email} or {profile_*} (i.e. any field in the user profile data).

+TXT; + +// @todo Make sure the state of the advanced toggle is saved in a template + + $advancedFieldsDefault = << 'textarea', 'required' => FALSE, ], + [ + 'name' => 'advancedFields', + 'caption' => 'Advanced fields', + 'description' => 'Fields that will be hidden unless the option to show advanced fields is selected.', + 'type' => 'textarea', + 'required' => FALSE, + 'default' => $advancedFieldsDefault, + ], [ 'name' => 'allowUpdates', 'caption' => 'Allow updates?', @@ -228,12 +278,14 @@ public static function get_form($args, $nid, $response = NULL) { 'writeAuth' => $auth['write_tokens'], 'fixedValues' => [], 'entity' => 'occurrence', + 'advancedFields' => '', ], $args); $options['fixedValues'] = empty($options['fixedValues']) ? [] : get_options_array_with_user_data($options['fixedValues']); $options['fixedValues'] = array_merge($options['fixedValues'], self::getAdditionalFixedValues($auth, $options['entity'])); if (!empty($options['fixedValueDefaults'])) { $options['fixedValueDefaults'] = get_options_array_with_user_data($options['fixedValueDefaults']); } + $options['advancedFields'] = helper_base::explode_lines($options['advancedFields']); return import_helper_2::importer($options); } @@ -251,6 +303,7 @@ public static function get_form($args, $nid, $response = NULL) { * Progress data from the warehouse. */ public static function ajax_upload_file($website_id, $password, $nid) { + iform_load_helpers(['import_helper_2']); return [ 'status' => 'ok', 'interimFile' => import_helper_2::uploadInterimFile(), diff --git a/prebuilt_forms/includes/groups.php b/prebuilt_forms/includes/groups.php index 61124cb3..cd02fd6a 100644 --- a/prebuilt_forms/includes/groups.php +++ b/prebuilt_forms/includes/groups.php @@ -80,18 +80,22 @@ function group_authorise_group_id($group_id, $readAuth) { 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(''); - } elseif (isset($gu[0]['administrator']) && isset($gp[0]['administrator'])) { + 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; diff --git a/prebuilt_forms/includes/report_filters.php b/prebuilt_forms/includes/report_filters.php index 8b256383..e79e3fa3 100644 --- a/prebuilt_forms/includes/report_filters.php +++ b/prebuilt_forms/includes/report_filters.php @@ -48,67 +48,99 @@ public function getTitle() { * @return string */ public function getControls(array $readAuth, array $options) { + global $indicia_templates; $r = ''; + // Optional tab texts. + $orDesignations = $options['elasticsearch'] ? '' : 'or designations, '; + $orScratchpads = empty($options['scratchpadSearch']) ? '' : 'or scratchpads, '; + // Language strings for emmitted HTML. + $lang = [ + 'buildListOfGroups' => lang::get('Build a list of groups'), + 'designations' => lang::get('Designations'), + 'includeMyGroups' => lang::get('Include my groups'), + 'level' => lang::get('Level'), + 'myGroups' => lang::get('My groups'), + 'otherFlags' => lang::get('Other flags'), + 'scratchpads' => lang::get('Scratchpads'), + 'selectAnyTab' => lang::get("Select the appropriate tab to filter by species group; or taxon name; {$orDesignations}or taxon level; {$orScratchpads}or other flags (such as marine, or non-native)."), + 'speciesGroups' => lang::get('Species groups'), + 'speciesGroupsFullListLink' => lang::get('Click here to show the full list'), + 'speciesGroupsIntro1' => lang::get('This tab allows you to choose one or more broad species groups. These have to match the group names that we use. Find a group by typing any part of its name, e.g. type "moth" to find "insect - moth".'), + 'speciesGroupsIntro2' => lang::get('Click on the group name from the dropdown list, then click on Add. You can add multiple groups. When you have added all the groups that you want, click Apply to filter the records.'), + 'speciesGroupsIntro3' => lang::get('Go to the "Species or higher taxa" tab to chose an individual species, genus or family etc.'), + 'speciesGroupsLimited' => lang::get('Please note that your access permissions are limiting the groups available to choose from.'), + 'speciesIntro1' => lang::get('This tab allows you to choose one or more individual species, or you can use genus, family, order etc. Type in a scientific or English name and click on the name you want from the dropdown list that appears. Then click on Add.'), + 'speciesIntro2' => lang::get('You can add multiple species or higher taxa. When you have added all the names that you want, click Apply to filter the records.'), + 'speciesLimited' => lang::get('Please note that your access permissions will limit the records returned to the species you are allowed to see.'), + 'speciesOrHigherTaxa' => lang::get('Species or higher taxa'), + ]; // There is only one tab when running on the Warehouse. if (!isset($options['runningOnWarehouse']) || $options['runningOnWarehouse'] == FALSE) { if (!empty($options['scratchpadSearch']) && $options['scratchpadSearch'] == TRUE) { $r .= "

" . lang::get('Select the appropriate tab to filter by species group, taxon name, ' . 'the level within the taxonomic hierarchy, a species scratchpad or other flags such as marine/terrestrial/freshwater or non-native taxa.') . "

\n"; - } else { - $r .= "

" . lang::get('Select the appropriate tab to filter by species group, taxon name, ' . - 'the level within the taxonomic hierarchy or other flags such as marine/terrestrial/freshwater or non-native taxa.') . "

\n"; + } + else { + $r .= "

$lang[selectAnyTab]

\n"; } } - $r .= '
' . "\n"; + $r .= "
\n"; + // Designations filter currently not available in ES. + $designationsTab = $options['elasticsearch'] ? '' : "
  • $lang[designations]
  • "; + // Scratchpads tab an optional extra. + $scratchpadsTab = empty($options['scratchpadSearch']) ? '' : "
  • $lang[scratchpads]
  • "; // Data_entry_helper::tab_header breaks inside fancybox. So output manually. - $r .= ''; - $r .= '
    ' . "\n"; + $r .= << + +HTML; + + // Species groups tab. if (function_exists('hostsite_get_user_field')) { $myGroupIds = hostsite_get_user_field('taxon_groups', [], TRUE); } else { $myGroupIds = []; } + $myGroupsPanel = ''; if ($myGroupIds) { - $r .= '

    ' . lang::get('My groups') . '

    '; $myGroupsData = data_entry_helper::get_population_data([ 'table' => 'taxon_group', 'extraParams' => $readAuth + [ 'query' => json_encode([ 'in' => ['id', $myGroupIds], ]), + 'columns' => 'id,title', ], ]); - $myGroupNames = []; - data_entry_helper::$javascript .= "indiciaData.myGroups = [];\n"; + $myGroupNamesLis = []; + data_entry_helper::$indiciaData['myGroups'] = $myGroupsData; foreach ($myGroupsData as $group) { - $myGroupNames[] = $group['title']; - data_entry_helper::$javascript .= "indiciaData.myGroups.push([$group[id],'$group[title]']);\n"; + $myGroupNamesLis[] = "
  • $group[title]
  • "; } - $r .= ''; - $r .= '
    • ' . implode('
    • ', $myGroupNames) . '
    '; - $r .= '

    ' . lang::get('Build a list of groups') . '

    '; + $myGroupNamesList = implode('', $myGroupNamesLis) . ''; + $myGroupsPanel = <<$lang[myGroups] + +
      + $myGroupNamesList +
    +HTML; } // Warehouse doesn't have master taxon list, so only need warning when not // running on warehouse. if (empty($options['taxon_list_id']) && (!isset($options['runningOnWarehouse']) || $options['runningOnWarehouse'] == FALSE)) { throw new exception('Please specify a @taxon_list_id option in the page configuration.'); } - $r .= '

    ' . lang::get('Search for and build a list of species groups to include') . '

    ' . - '
    ' . lang::get('Please note that your access permissions are limiting the groups available to choose from.') . '
    '; $baseParams = empty($options['taxon_list_id']) ? $readAuth : $readAuth + ['taxon_list_id' => $options['taxon_list_id']]; - $r .= data_entry_helper::sub_list([ + $taxonGroupsSubListCtrl = data_entry_helper::sub_list([ 'fieldname' => 'taxon_group_list', 'report' => 'library/taxon_groups/taxon_groups_used_in_checklist_lookup', 'captionField' => 'q', @@ -116,11 +148,32 @@ public function getControls(array $readAuth, array $options) { 'extraParams' => $baseParams, 'addToTable' => FALSE, 'continueOnBlur' => FALSE, + 'matchContains' => TRUE, ]); - $r .= "
    \n"; - $r .= '
    ' . "\n"; - $r .= '

    ' . lang::get('Search for and build a list of species or higher taxa to include.') . '

    ' . - '
    ' . lang::get('Please note that your access permissions will limit the records returned to the species you are allowed to see.') . '
    '; + data_entry_helper::$indiciaData['allTaxonGroups'] = data_entry_helper::get_population_data([ + 'report' => 'library/taxon_groups/taxon_groups_used_in_checklist', + 'extraParams' => $baseParams, + // Long cache timeout. + 'cacheTimeout' => 7 * 24 * 60 * 60, + ]); + $columns = str_replace(['{attrs}', '{col-1}', '{col-2}'], [ + '', + "

    $lang[buildListOfGroups]

    \n$taxonGroupsSubListCtrl", + $myGroupsPanel, + ], $indicia_templates['two-col-50']); + $r .= << + +
    $lang[speciesGroupsLimited]
    + $columns +
    +HTML; + + // Species tab. $subListOptions = [ 'fieldname' => 'taxa_taxon_list_list', 'autocompleteControl' => 'species_autocomplete', @@ -133,8 +186,19 @@ public function getControls(array $readAuth, array $options) { 'addToTable' => FALSE, 'continueOnBlur' => FALSE, ]; - $r .= data_entry_helper::sub_list($subListOptions); - $r .= "
    \n"; + $taxaSubListCtrl = data_entry_helper::sub_list($subListOptions); + $r .= << +
      +
    • $lang[speciesIntro1]
    • +
    • $lang[speciesIntro2]
    • +
    +
    $lang[speciesLimited]
    + $taxaSubListCtrl +
    +HTML; + + // Designations tab. if (!$options['elasticsearch']) { try { $r .= "
    \n"; @@ -156,7 +220,6 @@ public function getControls(array $readAuth, array $options) { } catch (Exception $e) { if (strpos($e->getMessage(), 'Unrecognised entity') !== FALSE) { - global $indicia_templates; $r .= '

    ' . str_replace(['{message}'], [lang::get('Designations functionality is not enabled on this server.')], $indicia_templates['messageBox']); } else { @@ -164,6 +227,8 @@ public function getControls(array $readAuth, array $options) { } } } + + // Ranks tab. $r .= "

    \n"; $r .= '

    ' . lang::get('Include records where the level') . '

    '; $r .= data_entry_helper::select([ @@ -186,12 +251,14 @@ public function getControls(array $readAuth, array $options) { ], 'cachePerUser' => FALSE, ]); - $r .= ''; foreach ($ranks as $rank) { $r .= ""; } $r .= ''; $r .= "
    \n"; + + // Scratchpads tab. if (!empty($options['scratchpadSearch']) && $options['scratchpadSearch'] == TRUE) { $r .= "
    \n"; $r .= '

    ' . lang::get('Select a species scratchpad to filter against') . '

    '; @@ -207,6 +274,8 @@ public function getControls(array $readAuth, array $options) { ]); $r .= "
    \n"; } + + // Flags tab. $r .= "
    \n"; $r .= '

    ' . lang::get('Select additional flags to filter for.') . '

    ' . '
    ' . lang::get('Please note that your access permissions limit the settings you can change on this tab.') . '
    '; @@ -498,6 +567,7 @@ public function getControls(array $readAuth, array $options) { 'valueField' => 'id', 'addToTable' => FALSE, 'extraParams' => $readAuth, + 'matchContains' => TRUE, ]); $r .= '
    '; @@ -623,9 +693,18 @@ public function getTitle() { */ public function getControls() { $r = '
    ' . lang::get('Please note, you cannnot change this setting because of your access permissions in this context.') . '
    '; - $r .= data_entry_helper::checkbox([ - 'label' => lang::get('Only include my records'), + $r .= data_entry_helper::radio_group([ + 'label' => lang::get('Recorders'), 'fieldname' => 'my_records', + 'lookupValues' => [ + '' => lang::get('Records from all recorders'), + '1' => lang::get('Only include my records'), + '0' => lang::get('Exclude my records'), + ], + ]); + $r .= data_entry_helper::text_input([ + 'label' => lang::get('Or, filter by name or part of name'), + 'fieldname' => 'recorder_name', ]); return $r; } @@ -741,74 +820,130 @@ public function getTitle() { /** * Define the HTML required for this filter's UI panel. */ - public function getControls($readAuth, $options, $ctls = ['status', 'auto', 'difficulty', 'photo']) { + public function getControls($readAuth, $options, $ctls = [ + 'status', + 'certainty', + 'auto', + 'difficulty', + 'photo', + 'licences', + 'media_licences', + 'coordinate_precision', + ]) { + global $indicia_templates; $r = ''; if (in_array('status', $ctls)) { $r .= '
    ' . lang::get('Please note, your options for quality filtering are restricted by your access permissions in this context.') . '
    '; $qualityOptions = [ - 'V1' => lang::get('Accepted as correct records only'), - 'V' => lang::get('Accepted records only'), - '-3' => lang::get('Reviewer agreed at least plausible'), - 'C3' => lang::get('Plausible records only'), - 'C' => lang::get('Recorder was certain'), - 'L' => lang::get('Recorder thought the record was at least likely'), - 'P' => lang::get('Not reviewed'), - 'T' => lang::get('Not reviewed but trusted recorder'), - '!R' => lang::get('Exclude not accepted records'), - '!D' => lang::get('Exclude queried or not accepted records'), + 'P' => lang::get('Pending'), + 'V' => lang::get('Accepted (all)'), + 'V1' => lang::get('Accepted - correct only'), + 'V2' => lang::get('Accepted - considered correct only'), + 'R' => lang::get('Not accepted (all)'), + 'R4' => lang::get('Not accepted - unable to verify only'), + 'R5' => lang::get('Not accepted - incorrect only'), + 'C3' => lang::get('Plausible'), + 'D' => lang::get('Queried'), 'all' => lang::get('All records'), - 'D' => lang::get('Queried records only'), - 'A' => lang::get('Answered records only'), - 'R' => lang::get('Not accepted records only'), - 'R4' => lang::get('Not accepted as reviewer unable to verify records only'), - 'DR' => lang::get('Queried or not accepted records'), ]; + if ($options['sharing'] === 'verification') { + $qualityOptions['OV'] = lang::get('Verified by other verifiers'); + $qualityOptions['A'] = lang::get('Answered'); + } + $qualityOptions['all'] = lang::get('All records'); if ($options['elasticsearch']) { // Elasticsearch doesn't currently support recorder trust. unset($qualityOptions['T']); + // Additional option available for Elasticsearch verification. + if ($options['sharing'] === 'verification') { + $qualityOptions['OV'] = lang::get('Verified by other verifiers'); + } } $options = array_merge([ - 'label' => lang::get('Records to include'), + 'label' => lang::get('Record status'), ], $options); - - $r .= data_entry_helper::select([ - 'label' => $options['label'], + $includeExcludeRadios = data_entry_helper::radio_group([ + 'fieldname' => 'quality_op', + 'id' => 'quality_op' . (empty($options['standalone']) ? '' : '--standalone'), + 'lookupValues' => [ + 'in' => lang::get('Include'), + 'not in' => lang::get('Exclude'), + ], + 'default' => 'in', + ]); + $qualityCheckboxes = data_entry_helper::checkbox_group([ 'fieldname' => 'quality', - 'id' => 'quality-filter', + 'id' => 'quality' . (empty($options['standalone']) ? '' : '--standalone'), 'lookupValues' => $qualityOptions, ]); + $lang = [ + 'cancel' => lang::get('Cancel'), + 'ok' => lang::get('Ok'), + 'recordStatus' => lang::get('Record status'), + ]; + $qualityInput = data_entry_helper::text_input([ + 'label' => $lang['recordStatus'], + 'fieldname' => 'quality-filter', + 'class' => 'quality-filter', + ]); + $r .= << + $qualityInput + + +
    +HTML; + } + if (in_array('certainty', $ctls)) { + $r .= data_entry_helper::checkbox_group([ + 'label' => lang::get('Recorder certainty'), + 'fieldname' => 'certainty[]', + 'id' => 'certainty-filter', + 'lookupValues' => [ + 'C' => lang::get('Certain'), + 'L' => lang::get('Likely'), + 'U' => lang::get('Uncertain'), + 'NS' => lang::get('Not stated'), + ], + ]); } if (in_array('auto', $ctls)) { $checkOptions = [ '' => lang::get('Not filtered'), - 'P' => lang::get('Only include records that pass all automated checks'), - 'F' => lang::get('Only include records that fail at least one automated check'), + 'P' => lang::get('All checks passed'), + 'F' => lang::get('Any checks failed'), ]; if (!empty($options['customRuleCheckFilters'])) { - $checkOptions['PC'] = lang::get('Only include records that have not been flagged by one of your custom rules.'); - $checkOptions['FC'] = lang::get('Only include records that have been flagged by one of your custom rules.'); + $checkOptions['PC'] = lang::get('All custom rule checks passed'); + $checkOptions['FC'] = lang::get('Any custom rule checks failed'); } - $r .= data_entry_helper::select([ - 'label' => empty($options['customRuleCheckFilters']) ? lang::get('Automated checks') : lang::get('Automated or custom rule checks'), - 'fieldname' => 'autochecks', - 'lookupValues' => $checkOptions, - ]); if (!empty($options['autocheck_rules'])) { - $ruleOptions = ['' => lang::get('Not filtered')]; foreach ($options['autocheck_rules'] as $rule) { - $ruleOptions[$rule] = lang::get($rule); + // ID diff handled separately. + if ($rule !== 'identification_difficulty') { + $checkOptions[$rule] = lang::get("$rule failed"); + } }; - $r .= data_entry_helper::select([ - 'label' => lang::get('Select records by specific automated check flags'), - 'fieldname' => 'autocheck_rule', - 'lookupValues' => $ruleOptions, - ]); } + $r .= data_entry_helper::select([ + 'label' => empty($options['customRuleCheckFilters']) + ? lang::get('Select records by specific type of automated check') + : lang::get('Select records by specific type of automated or custom rule check'), + 'fieldname' => 'autochecks', + 'lookupValues' => $checkOptions, + ]); } if (in_array('difficulty', $ctls)) { global $indicia_templates; $s1 = data_entry_helper::select([ - 'label' => lang::get('Identification difficulty'), + 'label' => lang::get('ID difficulty'), 'fieldname' => 'identification_difficulty_op', 'lookupValues' => [ '=' => lang::get('is'), @@ -817,35 +952,101 @@ public function getControls($readAuth, $options, $ctls = ['status', 'auto', 'dif ], ]); $s2 = data_entry_helper::select([ - 'label' => lang::get('Level'), 'fieldname' => 'identification_difficulty', 'lookupValues' => [ '' => lang::get('Not filtered'), - 1 => 1, - 2 => 2, - 3 => 3, - 4 => 4, - 5 => 5, + 1 => lang::get('difficulty 1 - easiest to ID'), + 2 => lang::get('difficulty 2'), + 3 => lang::get('difficulty 3'), + 4 => lang::get('difficulty 4'), + 5 => lang::get('difficulty 5 - hardest to ID'), + 6 => lang::get('difficulty 6 - custom check'), ], 'controlWrapTemplate' => 'justControl', ]); - $r .= str_replace( - ['{attrs}', '{col-1}', '{col-2}'], - ['', $s1, $s2], - $indicia_templates['two-col-50'] - ); + $r .= ''; + $r .= "
    $s1$s2
    "; } if (in_array('photo', $ctls)) { $r .= data_entry_helper::select([ - 'label' => 'Photos', + 'label' => lang::get('Records and photos'), 'fieldname' => 'has_photos', 'lookupValues' => [ - '' => 'Include all records', - '1' => 'Only include records which have photos', - '0' => 'Exclude records which have photos', + '' => lang::get('-No filter-'), + '1' => lang::get('With'), + '0' => lang::get('Without'), + ], + ]); + } + $licencesCtrl = ''; + $mediaLicencesCtrl = ''; + if (in_array('licences', $ctls)) { + $licencesCtrl = data_entry_helper::checkbox_group([ + 'label' => lang::get('Include records with'), + 'fieldname' => 'licences', + 'lookupValues' => [ + 'none' => lang::get('-No licence-'), + 'open' => lang::get('Open licence (OGL, CCO, CC BY)'), + 'restricted' => lang::get('Restricted licence (CC BY-NC)'), + ], + ]); + } + if (in_array('media_licences', $ctls)) { + $mediaLicencesCtrl = data_entry_helper::checkbox_group([ + 'label' => lang::get('Include records with photos that have'), + 'fieldname' => 'media_licences', + 'lookupValues' => [ + 'none' => lang::get('-No licence-'), + 'open' => lang::get('Open licence (OGL, CCO, CC BY)'), + 'restricted' => lang::get('Restricted licence (CC BY-NC)'), ], ]); } + //$r .= var_export($ctls, TRUE); + if ($licencesCtrl && $mediaLicencesCtrl) { + $r .= str_replace( + ['{attrs}', '{col-1}', '{col-2}'], + ['', $licencesCtrl, $mediaLicencesCtrl], + $indicia_templates['two-col-50'], + ); + } + else { + $r .= $licencesCtrl; + $r .= $mediaLicencesCtrl; + } + if (in_array('coordinate_precision', $ctls)) { + $lang = [ + 'coordinatePrecision' => lang::get('Coordinate precision'), + 'includeRecordsWhere' => lang::get('Include records where the grid ref precision'), + ]; + $r .= <<$lang[coordinatePrecision] +

    $lang[includeRecordsWhere]

    +HTML; + $opCtrl = data_entry_helper::radio_group([ + 'fieldname' => 'coordinate_precision_op', + 'lookupValues' => [ + '<=' => lang::get('is the same as or better than'), + '>' => lang::get('is worse than'), + '=' => lang::get('is equal to'), + ], + ]); + $coordSizeCtrl = data_entry_helper::radio_group([ + 'fieldname' => 'coordinate_precision', + 'lookupValues' => [ + '' => lang::get('Not filtered'), + '1000' => '1km', + '2000' => '2km', + '10000' => '10km', + '100000' => '100km', + ], + ]); + $r .= str_replace( + ['{attrs}', '{col-1}', '{col-2}'], + ['', $opCtrl, $coordSizeCtrl], + $indicia_templates['two-col-50'], + ); + } return $r; } @@ -874,7 +1075,7 @@ public function getControls() { $r .= data_entry_helper::select([ 'label' => lang::get('Samples to include'), 'fieldname' => 'quality', - 'id' => 'quality-filter', + 'class' => 'quality-filter', 'lookupValues' => [ 'V' => lang::get('Accepted records only'), 'P' => lang::get('Not reviewed'), @@ -962,14 +1163,17 @@ public function getControls(array $readAuth, array $options) { // Build the list filter control HTML. $websitesFilterInput = data_entry_helper::text_input([ 'fieldname' => 'websites-search', + 'class' => 'filter-exclude', 'attributes' => ['placeholder' => lang::get('Type here to filter')], ]); $surveysFilterInput = data_entry_helper::text_input([ 'fieldname' => 'surveys-search', + 'class' => 'filter-exclude', 'attributes' => ['placeholder' => lang::get('Type here to filter')], ]); $inputFormsFilterInput = data_entry_helper::text_input([ 'fieldname' => 'input_forms-search', + 'class' => 'filter-exclude', 'attributes' => ['placeholder' => lang::get('Type here to filter')], ]); // Build the filter operation controls. @@ -1067,20 +1271,29 @@ private function getOperationSelectInput($type) { } -function status_control($readAuth, $options) { +/** + * Output a standalone media/photos drop-down filter. + */ +function media_filter_control($readAuth, $options) { + iform_load_helpers(['report_helper']); + report_helper::add_resource('reportfilters'); + $ctl = new filter_quality(); + $r = '
    '; + $r .= $ctl->getControls($readAuth, $options, ['photo']); + $r .= '
    '; + return $r; +} + +/** + * Output a standalone status drop-down filter. + */ +function status_filter_control($readAuth, $options) { iform_load_helpers(['report_helper']); report_helper::add_resource('reportfilters'); $ctl = new filter_quality(); $r = '
    '; $r .= $ctl->getControls($readAuth, $options, ['status']); $r .= '
    '; - - report_helper::$onload_javascript .= << FALSE, 'customRuleCheckFilters' => FALSE, 'autocheck_rules' => [ + 'ancillary_species', 'identification_difficulty', 'period', 'period_within_year', @@ -1674,14 +1888,38 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu report_helper::addLanguageStringsToJs('reportFiltersNoDescription', $noDescriptionLangStrings); report_filters_set_parser_language_strings(); report_helper::addLanguageStringsToJs('reportFilters', [ - 'PleaseSelect' => 'Please select', - 'CreateAFilter' => 'Create a filter', - 'ModifyFilter' => 'Modify filter', - 'FilterSaved' => 'The filter has been saved', - 'FilterDeleted' => 'The filter has been deleted', - 'ConfirmFilterChangedLoad' => 'Do you want to load the selected filter and lose your current changes?', - 'FilterExistsOverwrite' => 'A filter with that name already exists. Would you like to overwrite it?', - 'ConfirmFilterDelete' => 'Are you sure you want to permanently delete the {title} filter?', + 'back' => 'Back', + 'cannotDeselectAllLicences' => 'You cannot deselect all licence options otherwise no records will be returned.', + 'cannotDeselectAllMediaLicences' => 'You cannot deselect all photo licence options otherwise no records with photos will be returned.', + 'confirmFilterChangedLoad' => 'Do you want to load the selected filter and lose your current changes?', + 'confirmFilterDelete' => 'Are you sure you want to permanently delete the {title} filter?', + 'coordinatePrecisionIs' => 'Coordinate precision is', + 'createAFilter' => 'Create a filter', + 'quality:P' => 'Pending', + 'quality:V' => 'Accepted (all)', + 'quality:V1' => 'Accepted - correct only', + 'quality:V2' => 'Accepted - considered correct only', + 'quality:R' => 'Not accepted (all)', + 'quality:R4' => 'Not accepted - unable to verify only', + 'quality:R5' => 'Not accepted - incorrect only', + 'quality:C3' => 'Plausible', + 'quality:D' => 'Queried', + 'quality:A' => 'Answered', + 'quality:all' => 'All records', + 'quality_op:in' => 'Include', + 'quality_op:not in' => 'Exclude', + 'filterDeleted' => 'The filter has been deleted', + 'filterExistsOverwrite' => 'A filter with that name already exists. Would you like to overwrite it?', + 'filterSaved' => 'The filter has been saved', + 'licenceIs' => 'Licence is', + 'mediaLicenceIs' => 'Media licence is', + 'modifyFilter' => 'Modify filter', + 'orListJoin' => ' or ', + 'pleaseSelect' => 'Please select', + 'recorderCertaintyWas' => 'Recorder certainty was', + 'sameAsOrBetterThan' => 'same as or better than', + 'worseThan' => 'worse than', + 'equalTo' => 'equal to', ]); if (function_exists('iform_ajaxproxy_url')) { report_helper::$javascript .= "indiciaData.filterPostUrl='" . iform_ajaxproxy_url(NULL, 'filter') . "';\n"; @@ -1713,13 +1951,16 @@ function report_filter_panel(array $readAuth, $options, $website_id, &$hiddenStu $optionParams[substr($key, 7)] = $value; } } - $allParams = array_merge(['quality' => '!R'], $optionParams, $getParams); + $allParams = array_merge(['quality' => 'R', 'quality_op' => 'not in'], $optionParams, $getParams); if (!empty($allParams)) { report_helper::$initialFilterParamsToApply = array_merge(report_helper::$initialFilterParamsToApply, $allParams); $json = json_encode($allParams); - report_helper::$onload_javascript .= "var params = $json;\n"; - report_helper::$onload_javascript .= "indiciaData.filter.def=$.extend(indiciaData.filter.def, params);\n"; - report_helper::$onload_javascript .= "indiciaData.filter.resetParams = $.extend({}, params);\n"; + report_helper::$onload_javascript .= << 'Automated checks failed', - 'AutochecksP' => 'Automated checks passed', - 'AutochecksFC' => 'Flagged by a custom verification rule', - 'AutochecksPC' => 'Not flagged by a custom verification rule', + 'Autochecks_ancillary_species' => 'Rarity check failed', + 'Autochecks_F' => 'Automated checks failed', + 'Autochecks_FC' => 'Any custom verification rule check failed', + 'Autochecks_identification_difficulty' => 'ID difficulty check failed', + 'Autochecks_P' => 'Automated checks passed.', + 'Autochecks_period' => 'Year range check failed', + 'Autochecks_period_within_year' => 'Date range check failed', + 'Autochecks_PC' => 'All custom verification rule checks passed.', + 'Autochecks_without_polygon' => 'Distribution check failed', 'IdentificationDifficulty' => 'Identification difficulty', 'HasPhotos' => 'Only include records which have photos', 'HasNoPhotos' => 'Exclude records which have photos', - 'MyRecords' => 'My records only', + 'ListJoin' => ' or ', + 'MyRecords' => 'Only include my records', + 'NotMyRecords' => 'Exclude my records', + 'RecorderNameContains' => 'Recorder name contains {1}', 'OnlyConfidentialRecords' => 'Only confidential records', 'AllConfidentialRecords' => 'Include both confidential and non-confidential records', 'NoConfidentialRecords' => 'Exclude confidential records', 'includeUnreleasedRecords' => 'Include unreleased records', 'excludeUnreleasedRecords' => 'Exclude unreleased records', - 'Rule_identification_difficulty' => 'Has an identification difficulty flag', - 'Rule_period' => 'Has a time period flag', - 'Rule_period_within_year' => 'Has a period within year flag', - 'Rule_without_polygon' => 'Has a range flag', ]); } diff --git a/prebuilt_forms/js/dynamic_taxon.js b/prebuilt_forms/js/dynamic_taxon.js index 4bba55e1..9959007c 100644 --- a/prebuilt_forms/js/dynamic_taxon.js +++ b/prebuilt_forms/js/dynamic_taxon.js @@ -345,7 +345,7 @@ indiciaFns.hookDynamicAttrsAfterLoad = []; '' + designationOptions + '').appendTo(tr); - $('').appendTo(tr); $('').appendTo(tr); @@ -353,8 +353,6 @@ indiciaFns.hookDynamicAttrsAfterLoad = []; 'name="taxon-designation-geographical_constraint:' + rowCount + '">').appendTo(tr); // Button to remove row. $('').appendTo(tr); - // Hook up datepicker. - $(tr).find('.taxon-designation-start_date').datepicker(); return tr; } diff --git a/prebuilt_forms/js/ebms_atlas_map.js b/prebuilt_forms/js/ebms_atlas_map.js new file mode 100644 index 00000000..9023e4de --- /dev/null +++ b/prebuilt_forms/js/ebms_atlas_map.js @@ -0,0 +1,200 @@ +jQuery(document).ready(function($) { + + console.log('Page JS loaded') + if (typeof(d3)=== 'undefined') return // Prevent code running when edit form displayed + + let brcmap_e, mapData + let week = 1 + let pp = false + let dataLoaded = false + + // Reset page title + // const pageTitle = d3.select('#preferred-name').property('value') + // d3.select('.page-header span').text(pageTitle) + + if (typeof(brcatlas_e) !== "undefined") { + + const mapOpts = { + selector: "#ebms-tracemap", + outputWidth: 900, + outputHeight: 400, + mapBB: [1000000, 800000, 6000000, 5500000], // [minx, miny, maxx, maxy] + expand: true, + fillEurope: '#1a1a20', + fillWorld: '#1a1a20', + fillOcean: '#3a3d4a', + strokeEurope: '#27272d', + fillDot: d3.select('#dot-colour').property('value'), + dotSize1: 1, + dotSize2: 3, + dotSize3: 6, + dotOpacity1: 1, + dotOpacity2: 0.4, + dotOpacity3: 0.1, + hightlightAllEurrope: true, + aggregate: false + } + + brcmap_e = brcatlas_e.eSvgMap(mapOpts) + + console.log('external-key', d3.select('#external-key').property('value')) + + if (d3.select('#external-key').property('value')) { + brcmap_e.showBusy(true) + } + } + + indiciaFns.processTraceMapData2 = function (el, sourceSettings, response) { + console.log(response.aggregations.aggs.buckets) + const mapData = [] + response.aggregations.aggs.buckets.forEach(wb => { + const week = Number(wb.key) + wb.geohash.buckets.forEach (ghb => { + const latlon = geohashToWkt(ghb.key) + mapData.push({ + week: week, + // Getting lat/lon from ES aggregation doesn't seem + // to give centroid of geohash as espected, so instead + // we use one worked out directly from geohash. + //lat: Number(ghb.centroid.location.lat), + //lon: Number(ghb.centroid.location.lon) + lat: latlon.lat, + lon: latlon.lon + }) + }) + }) + console.log('data', mapData) + brcmap_e.loadData(mapData) + brcmap_e.mapData(1) + brcmap_e.showBusy(false) + dataLoaded = true + } + + indiciaFns.processTraceMapData = function (el, sourceSettings, response) { + + //console.log('ES response', response) + mapData = response.hits.hits.map(d => { + return { + year: Number(d._source.event.year.replace(',','')), + week: Number(d._source.event.week), + lat: Number(d._source.location.point.split(',')[0]), + lon: Number(d._source.location.point.split(',')[1]) + } + }) + console.log('data', mapData) + brcmap_e.loadData(mapData) + brcmap_e.mapData(1) + brcmap_e.showBusy(false) + dataLoaded = true + } + + indiciaFns.playPause = function () { + pp = !pp + if (pp) { + d3.select('#playPause').text("||") + } else { + d3.select('#playPause').text(">") + } + } + + // If initplay is set, start the animation + if (d3.select('#initplay').property('value') === 'yes') { + indiciaFns.playPause() + } + + indiciaFns.displayWeek = function (e) { + week = Number(e) + brcmap_e.mapData(week) + d3.select("#weekNo").text(brcmap_e.getWeekDates(week)) + } + + indiciaFns.allYearsCheckboxClicked = function() { + + if (d3.select('#data-year-allyears').property('checked')) { + currentVal = d3.select('#data-year-filter').property('value') + d3.select('#data-year-filter').property('value', '') + d3.select('#data-year-filter').property('disabled', true) + } else { + d3.select('#data-year-filter').property('value', d3.select('#data-year-filter').property('max')) + d3.select('#data-year-filter').property('disabled', false) + } + + } + + // Reset GET form URL parameters before submission (species selector) + indiciaFns.speciesDetailsSub = () => { + brcmap_e.showBusy(true) + + let taxa_taxon_list_id + if ($('#occurrence\\:taxa_taxon_list_id').val() != '') { + taxa_taxon_list_id = $('#occurrence\\:taxa_taxon_list_id').val() + } else if ($('#taxa_taxon_list_id').val() != '') { + taxa_taxon_list_id = $('#taxa_taxon_list_id').val() + } + + $('#ebms-atlas-map-form #taxa_taxon_list_id').val(taxa_taxon_list_id) + if ($('#data-year-filter').val()) { + $('#ebms-atlas-map-form #data_year_filter').val($('#data-year-filter').val()) + } + + document.forms["ebms-atlas-map-form"].submit() + } + + function geohashToWkt(geohash) { + var minLat = -90; + var maxLat = 90; + var minLon = -180; + var maxLon = 180; + var shift; + var isForMin; + var isForLon = true; + var centreLon; + var centreLat; + var mask; + // The geohash alphabet. + const ghs32 = '0123456789bcdefghjkmnpqrstuvwxyz'; + for (var i = 0; i < geohash.length; i++) { + const chr = geohash.charAt(i); + const idx = ghs32.indexOf(chr); + if (idx === -1) { + throw new Error('Invalid character in geohash'); + } + for (shift = 4; shift >= 0; shift--) { + // Test bit at position shift. If 1, then for min, else for max. + mask = 1 << shift; + isForMin = idx & mask; + // Bits extracted from characters toggle between x & y. + if (isForLon) { + centreLon = (minLon + maxLon) / 2; + if (isForMin) { + minLon = centreLon; + } else { + maxLon = centreLon; + } + } else { + centreLat = (minLat + maxLat) / 2; + if (isForMin) { + minLat = centreLat; + } else { + maxLat = centreLat; + } + } + isForLon = !isForLon; + } + } + return { + lat: minLat + (maxLat - minLat) / 2, + lon: minLon + (maxLon - minLon) / 2 + } + //return 'POLYGON((' + minLon + ' ' + minLat + ',' + maxLon + ' ' + minLat + ', ' + maxLon + ' ' + maxLat + ', ' + minLon + ' ' + maxLat + ', ' + minLon + ' ' + minLat + '))'; + } + + // Kick of slider animation setInterval + setInterval(function() { + if (pp && dataLoaded) { + week = week === 52 ? 1 : week+1 + document.querySelector('.slider').value = week + indiciaFns.displayWeek(week) + } + }, 300) +}) \ No newline at end of file diff --git a/prebuilt_forms/lang/dynamic.de.php b/prebuilt_forms/lang/dynamic.de.php index fa0c35ab..54682b8d 100644 --- a/prebuilt_forms/lang/dynamic.de.php +++ b/prebuilt_forms/lang/dynamic.de.php @@ -1,5 +1,9 @@ 'Save', 'LANG_Blank_Text' => 'Select...', 'validation_required' => 'Dieses Feld ist erforderlich.', - 'Click here' => 'Hier klicken' -); \ No newline at end of file + 'Click here' => 'Hier klicken', +]; diff --git a/prebuilt_forms/lang/mnhnl_dynamic_2.en.php b/prebuilt_forms/lang/mnhnl_dynamic_2.en.php index e42551b3..7268499a 100644 --- a/prebuilt_forms/lang/mnhnl_dynamic_2.en.php +++ b/prebuilt_forms/lang/mnhnl_dynamic_2.en.php @@ -1,5 +1,9 @@ 'Surveys', 'LANG_SampleListGrid_Preamble' => 'Previously encoded survey list for ', 'LANG_All_Users' => 'all users', 'LANG_Allocate_Locations' => 'Allocate squares', 'LANG_Data_Download' => 'These reports provide details of the data entered in the survey.', - 'LANG_TargetSpecies'=> 'Target species', + 'LANG_TargetSpecies' => 'Target species', 'Target Species' => 'Target species', - 'LANG_SHP_Download_Legend'=> 'SHP File Downloads', - 'LANG_Shapefile_Download'=> 'These downloads provide zipped up shape files for the locations; due to the restrictions of the SHP file format, there are separate downloads for each of points, lines and polygons. Click to select:', + 'LANG_SHP_Download_Legend' => 'SHP File Downloads', + 'LANG_Shapefile_Download' => 'These downloads provide zipped up shape files for the locations; due to the restrictions of the SHP file format, there are separate downloads for each of points, lines and polygons. Click to select:', 'LANG_Edit' => 'Edit', 'LANG_Add_Sample' => 'Add new sample', 'LANG_Add_SubSample' => 'Add New Occurrence', @@ -46,17 +48,15 @@ 'LANG_CommonInstructions1' => 'Choose a square (5x5km) by either picking it from the drop down list, or clicking it on the map. This square will then be highlighted on the map, along with all existing sites associated with that square.', 'LANG_CommonParentLabel' => 'Square (5x5km)', 'LANG_CommonParentBlank' => 'Choose a square', - 'LANG_LocModTool_Instructions2'=>"Either click on the map (ensuring that the select tool on the map is active) to select the site you wish to modify, or choose from the drop down list. You may then change its name, or modify or add Points, Lines, or Polygons to define the site shape. You must choose the correct draw tool on the map for each of these. You may drag the highlighted vertices. To delete a point (purple circle) or shape vertex (red circle), place the mouse over the circle and press the 'Delete' button.
    Selecting an existing site, re-clicking the 'Start a new site' (tick) button or clicking the 'Remove the selected new site' (red cross) button will remove any new site.", + 'LANG_LocModTool_Instructions2' => "Either click on the map (ensuring that the select tool on the map is active) to select the site you wish to modify, or choose from the drop down list. You may then change its name, or modify or add Points, Lines, or Polygons to define the site shape. You must choose the correct draw tool on the map for each of these. You may drag the highlighted vertices. To delete a point (purple circle) or shape vertex (red circle), place the mouse over the circle and press the 'Delete' button.
    Selecting an existing site, re-clicking the 'Start a new site' (tick) button or clicking the 'Remove the selected new site' (red cross) button will remove any new site.", 'LANG_LocModTool_CantCreate' => "You can't create a new site using this tool - that has to be done within the survey data entry itself.", - 'LANG_DE_Instructions2'=>"To choose a site, either click the relevant site on the map (ensuring that the select tool on the map is active) or pick it from the drop down list (then the selected site is highlighted in blue on the map).
    You may add a new site: ensure a square has been selected, click the 'Start a new site' button on the map, select the map tool for the type of item you wish to draw, and draw on the map, clicking on each point. Double click on the final point of a line or polygon to complete it. At this point you will notice some small red circles appear around the newly drawn feature: you can change the boundary by dragging these circles. To delete a point (purple circle) or shape vertex (red circle), place the mouse over the circle and press the 'Delete' button.
    Selecting an existing site, re-clicking the 'Start a new site' (tick) button or clicking the 'Remove the selected new site' (red cross) button will remove any new site.
    It is not possible to change a site name or boundary on this form once it has been saved - this can be done by an Admin user using their special tool.", + 'LANG_DE_Instructions2' => "To choose a site, either click the relevant site on the map (ensuring that the select tool on the map is active) or pick it from the drop down list (then the selected site is highlighted in blue on the map).
    You may add a new site: ensure a square has been selected, click the 'Start a new site' button on the map, select the map tool for the type of item you wish to draw, and draw on the map, clicking on each point. Double click on the final point of a line or polygon to complete it. At this point you will notice some small red circles appear around the newly drawn feature: you can change the boundary by dragging these circles. To delete a point (purple circle) or shape vertex (red circle), place the mouse over the circle and press the 'Delete' button.
    Selecting an existing site, re-clicking the 'Start a new site' (tick) button or clicking the 'Remove the selected new site' (red cross) button will remove any new site.
    It is not possible to change a site name or boundary on this form once it has been saved - this can be done by an Admin user using their special tool.", 'LANG_LocModTool_IDLabel' => 'Old site name', 'LANG_DE_LocationIDLabel' => 'Site', 'LANG_CommonChooseParentFirst' => 'Choose a square first, before picking a site.', 'LANG_NoSitesInSquare' => 'There are no sites currently associated with this square', 'LANG_NoSites' => 'There are currently no sites defined: please create a new one.', 'LANG_CommonEmptyLocationID' => 'Choose an existing site', - 'LANG_Location_X_Label' => 'Site centre coordinates: X', - 'LANG_Location_Y_Label' => 'Y', 'LANG_LatLong_Bumpf' => '(LUREF geographical system, in metres)', 'LANG_CommonLocationNameLabel' => 'Site name', 'LANG_LocModTool_NameLabel' => 'New site name', @@ -66,8 +66,8 @@ 'LANG_LocationModTool_CommentLabel' => 'Comment', 'LANG_Location_Name_Blank_Text' => 'Choose a location using its name', 'LANG_Outside_Square_Reports' => 'Outside Square Checks', - 'LANG_Outside_Square_Download_1'=> 'This report provides a list of locations whose centres are outside their parent square', - 'LANG_Outside_Square_Download_2'=> 'This report provides a list of locations which have any part of their boundaries outside the boundaries of their parent square', + 'LANG_Outside_Square_Download_1' => 'This report provides a list of locations whose centres are outside their parent square', + 'LANG_Outside_Square_Download_2' => 'This report provides a list of locations which have any part of their boundaries outside the boundaries of their parent square', 'LANG_LocModTool_ParentLabel' => 'New site square', 'LANG_PositionInDifferentParent' => 'The position you have chosen is outside the selected parent. Do you wish to change the parent field to match the point?', @@ -78,8 +78,8 @@ 'LANG_PointOutsideParent' => 'Warning: the point you have created for your site is outside the square.', 'LANG_LineOutsideParent' => 'Warning: the line you have created for your site has a centre which is outside the square.', 'LANG_PolygonOutsideParent' => 'Warning: the polygon you have created for new site has a centre which is outside the square.', - 'LANG_ConfirmRemoveDrawnSite'=> "This action will remove the existing site you have created. Do you wish to continue?", - 'LANG_ChoseParentWarning'=> "You can only add a new site after picking a square.", + 'LANG_ConfirmRemoveDrawnSite' => "This action will remove the existing site you have created. Do you wish to continue?", + 'LANG_ChoseParentWarning' => "You can only add a new site after picking a square.", 'LANG_SelectTooltip' => 'Click on map to select a site (or a square if present)', 'LANG_PolygonTooltip' => 'Draw polygon(s) for the site', 'LANG_LineTooltip' => 'Draw line(s) for the site', @@ -103,7 +103,7 @@ 'LANG_HighlightPoint' => 'Highlight this point', 'Location Comment' => 'Comment', -// Georeferencing + // Georeferencing. 'search' => 'Search', 'LANG_Georef_Label' => 'Search for a place on the map', 'LANG_Georef_SelectPlace' => 'Select the correct one from the following places that were found matching your search. (Click on the list items to see them on the map.)', @@ -112,7 +112,7 @@ 'LANG_Date' => 'Date', 'Overall Comment' => 'Comments', 'Recorder names' => 'Observer(s)', - 'LANG_RecorderInstructions'=>"To select more than one observer, keep the CTRL button down.", + 'LANG_RecorderInstructions' => 'To select more than one observer, keep the CTRL button down.', 'Reptile Visit' => 'Visit', 'Duration' => 'Duration (minutes)', 'Unsuitability' => 'Unsuitable site for target species', @@ -120,26 +120,22 @@ 'Weather' => 'Weather conditions', 'Temperature' => 'Temperature (° Celsius)', 'Temperature (Celsius)' => 'Temperature (° Celsius)', -// Cloud cover -// 'Rain' => 'Rain', 'LANG_Tab_species' => 'Species', - 'LANG_SpeciesInstructions'=>"Additional species may be added using the control under the grid.
    Additional rows may be added using the control for existing taxa if a different combination of Type/Stage/Sex/Behaviour is to be added.
    There are various combinations Type/Stage/Sex/Behaviour which are not allowed (eg an 'egg' can not be a 'dead specimen'). Such banned combinations will be greyed out in the drop down lists. In addition, it is not possible to enter multiple rows for the same combination of Species/Type/Stage/Sex/Behaviour: again duplicate possiblities will be greyed out.
    If you think a combination is valid, but you can not select it, first check that there is no other existing row with this combination.
    The 'No observation' can only be selected when there are no undeleted rows in the grid (when it must be selected) - otherwise it is disabled. Click the red 'X' to delete the relevant row.", + 'LANG_SpeciesInstructions' => "Additional species may be added using the control under the grid.
    Additional rows may be added using the control for existing taxa if a different combination of Type/Stage/Sex/Behaviour is to be added.
    There are various combinations Type/Stage/Sex/Behaviour which are not allowed (eg an 'egg' can not be a 'dead specimen'). Such banned combinations will be greyed out in the drop down lists. In addition, it is not possible to enter multiple rows for the same combination of Species/Type/Stage/Sex/Behaviour: again duplicate possiblities will be greyed out.
    If you think a combination is valid, but you can not select it, first check that there is no other existing row with this combination.
    The 'No observation' can only be selected when there are no undeleted rows in the grid (when it must be selected) - otherwise it is disabled. Click the red 'X' to delete the relevant row.", 'species_checklist.species' => 'Species', 'Count' => 'Number', 'Occurrence reliability' => 'Reliability', 'Reliability' => 'Survey reliability', - // Can also add entries for 'Yes' and 'No' for the voucher attribute + // Can also add entries for 'Yes' and 'No' for the voucher attribute. 'LANG_Record_Status_Label' => 'Record Status', 'LANG_Record_Status_I' => 'In Progress', 'LANG_Record_Status_C' => 'Completed', - 'LANG_Record_Status_V' => 'Verified', // NB not used + 'LANG_Record_Status_V' => 'Verified', 'LANG_Image_Label' => 'Upload Image', - 'LANG_Save' => 'Save', 'LANG_Save_Redisplay' => 'Save and Redisplay', 'LANG_Save_and_New' => 'Save then Enter New Record', - 'LANG_Cancel' => 'Cancel', 'LANG_Supersample_Layer' => 'Parent', 'LANG_Subsample_Layer' => 'Children', @@ -161,6 +157,5 @@ 'validation_end_time' => 'The end time must be after the start time', 'validation_integer' => 'Please enter an integer', 'next step' => 'Next step', - 'prev step' => 'Previous step' - -); \ No newline at end of file + 'prev step' => 'Previous step', +]; diff --git a/prebuilt_forms/species_details_2.php b/prebuilt_forms/species_details_2.php index 0dddefd9..03237737 100644 --- a/prebuilt_forms/species_details_2.php +++ b/prebuilt_forms/species_details_2.php @@ -34,6 +34,7 @@ * - Species Details including custom attributes. * - An Explore Species' Records button that links to a custom URL. * - Any photos of occurrences with the same meaning as the taxon. + * - A list of recording schemes & societies associated by the taxon (in the UKSI). * - A map displaying occurrences of taxa with the same meaning as the taxon. * - A bar chart indicating the accummulation of records over time. * - A phenology chart showing the accummulation of records through the year. @@ -267,6 +268,7 @@ public static function get_parameters() { "[control name] indicates a predefined control is to be added to the form with the following predefined controls available:
    " . "  [species] - a taxon selection control.
    " . "  [speciesdetails] - displays information relating to the occurrence and its sample
    " . + "  [rss] - displays a list of recording schemes & societies associated with the taxon
    " . "  [explore] - a button “Explore this species' records” which takes you to explore all records, filtered to the species.
    " . "  [photos] - photos associated with the occurrence
    " . "  [hectadmap] - a hectad distribution overview map for the taxon
    " . @@ -282,6 +284,7 @@ public static function get_parameters() { 'default' => ' =General= [speciesdetails] +[rss] [photos] [explore] | @@ -1281,6 +1284,48 @@ protected static function get_hectadmap_html($auth, $args, $tabalias, $options) return $r; } + /** + * Output RSS information. + * + * @return string + */ + protected static function get_control_rss($auth, $args, $tabalias, $options) { + + if (isset(self::$taxonMeaningId)) { + //dpm('taxonMeaningId' . ' ' . self::$taxonMeaningId); + + iform_load_helpers(['report_helper']); + + $extraParams['taxon_meaning_id'] = self::$taxonMeaningId; + $extraParams['taxon_list_id'] = 15; + + $rss = report_helper::get_report_data([ + 'readAuth' => $auth['read'], + 'dataSource' => 'library/taxa/taxon_rss', + 'useCache' => TRUE, + 'extraParams' => $extraParams, + ]); + + //dpm($rss); + + $r = '

    ' . lang::get('Recording schemes & societies') . '

    '; + + if (count($rss) > 0) { + $r .= '
      '; + foreach ($rss as $scheme) { + $r .= '
    • ' . $scheme['title'] . '
    • '; + } + $r .= '
    '; + } else { + $r .= '
    No societies are listed in the UK Species Inventory for this taxon.
    '; + } + $r .= '
    '; + } else { + $r = ''; + } + return $r; + } + /** * Draw Photos section of the page. * @@ -1762,6 +1807,7 @@ protected static function getArgDefaults($args) { if (!isset($args['structure']) || empty($args['structure'])) { $args['structure'] = '=General= [speciesdetails] +[rss] [photos] [explore] | diff --git a/report_helper.php b/report_helper.php index 2d85595d..21a39ff4 100644 --- a/report_helper.php +++ b/report_helper.php @@ -3093,7 +3093,9 @@ function rebuild_page_url(oldURL, overrideparam, overridevalue, removeparam) { setlocale (LC_TIME, $lang); for($i=0; $i<7; $i++){ - $r .= "" . lang::get(utf8_encode(date('D', $header_date->getTimestamp()))) . ""; // i8n + $r .= "" . + lang::get(mb_convert_encoding(date('D', $header_date->getTimestamp()), 'UTF-8', 'ISO-8859-1')) . + ""; // i8n $header_date->modify('+1 day'); } $r .= ""; @@ -3132,7 +3134,11 @@ function rebuild_page_url(oldURL, overrideparam, overridevalue, removeparam) { while($consider_date->format('Y') <= $options["year"] && ($weeknumberfilter[1]=='' || $consider_date->format('N')!=$weekstart[1] || $weekno < $weeknumberfilter[1])){ if($consider_date->format('N')==$weekstart[1]) { $weekno++; - $r .= "".($options['includeWeekNumber'] ? "".$weekno."" : "")."" . t(utf8_encode(date('M', $consider_date->getTimestamp()))) . ""; + $r .= "" . + ($options['includeWeekNumber'] ? "" . $weekno . "" : "") . + "" . + t(mb_convert_encoding(date('M', $consider_date->getTimestamp()), 'UTF-8', 'ISO-8859-1')) . + ""; } $cellContents=$consider_date->format('j'); // day in month. $cellclass=""; diff --git a/user_helper.php b/user_helper.php index 2f4131fe..d21ad024 100644 --- a/user_helper.php +++ b/user_helper.php @@ -127,7 +127,7 @@ class user_helper extends helper_base { * @return string HTML to insert into the page for the login control. */ - public static function login_control($options = [] + public static function login_control($options = []) { $r = ''; $method = (array_key_exists('control_method', $options)) ? ' method="'.$options['control_method'].'"' : ' method="post"'; $id = (array_key_exists('control_id', $options)) ? ' id="'.$options['control_id'].'"' : ' id="indicia-login-control"';