From ec03418116a5929554d7a2d42e3544c349d54d57 Mon Sep 17 00:00:00 2001 From: John van Breda Date: Mon, 22 Aug 2022 09:29:43 +0100 Subject: [PATCH 1/9] REST API documentation improvements --- modules/rest_api/i18n/en_GB/rest_api.php | 13 +++++++++++-- modules/rest_api/libraries/RestApiResponse.php | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/rest_api/i18n/en_GB/rest_api.php b/modules/rest_api/i18n/en_GB/rest_api.php index 356bb7eac3..ff231840cb 100644 --- a/modules/rest_api/i18n/en_GB/rest_api.php +++ b/modules/rest_api/i18n/en_GB/rest_api.php @@ -90,6 +90,11 @@ then saved into the current record as a foreign key.

HTML; $lang['resourcesTitle'] = 'Resources'; +$lang['resourcesIntroduction'] = <<online recording REST API documentation. TXT; $lang['resources']['GET annotations'] = 'Retrieve a list of annotations available to this client ID.'; $lang['resources']['GET annotations/{id}'] = <<online recording REST API documentation. +TXT; $lang['resources']['POST taxon-observations'] = 'Creates an occurrence using the deprecated NBN Gateway exchange format.'; $lang['resources']['GET reports'] = << kohana::lang("rest_api.authIntroduction"), 'authMethods' => kohana::lang("rest_api.authMethods"), 'resources' => kohana::lang("rest_api.resourcesTitle"), + 'resourcesIntro' => kohana::lang("rest_api.resourcesIntroduction"), 'filters' => kohana::lang("rest_api.filterTitle"), 'filterText' => kohana::lang("rest_api.filterText"), 'submissionFormat' => kohana::lang("rest_api.submissionFormatTitle"), @@ -193,6 +194,7 @@ private function indexHtml($resourceConfig) {

$lang[submissionFormat]

$lang[submissionFormatText]

$lang[resources]

+

$lang[resourcesIntro]

HTML; $apiRoot = url::base() . 'index.php/services/rest'; @@ -208,6 +210,9 @@ private function indexHtml($resourceConfig) { } $badge = empty($endpointPathOptions['deprecated']) ? '' : ' deprecated'; $endpointOutput .= "

$method $endpointPath$badge

"; + if (!empty($endpointPathOptions['deprecated'])) { + $endpointOutput .= '

' . kohana::lang('rest_api.deprecatedEndpoint') . '

'; + } $endpointOutput .= "

Example URL: $apiRoot/" . str_replace( ['{id}', '{path}', '{file.xml}'], ['123', 'library/occurrences', 'filterable_explore_list.xml'], From 8a18564b64e2516a126e4bd3de3ca8268066a3be Mon Sep 17 00:00:00 2001 From: John van Breda Date: Thu, 25 Aug 2022 15:56:48 +0100 Subject: [PATCH 2/9] Bool params to REST api reports accept t/f Allows autofeed to be a validated and documented param. --- modules/rest_api/controllers/services/rest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/rest_api/controllers/services/rest.php b/modules/rest_api/controllers/services/rest.php index f016902e03..2c0bc39b2a 100644 --- a/modules/rest_api/controllers/services/rest.php +++ b/modules/rest_api/controllers/services/rest.php @@ -457,12 +457,18 @@ class Rest_Controller extends Controller { 'reports/{path}' => [], 'reports/{path}/{file.xml}' => [ 'params' => [ + 'autofeed' => [ + 'datatype' => 'boolean', + ], 'filter_id' => [ 'datatype' => 'integer', ], 'limit' => [ 'datatype' => 'integer', ], + 'max_time' => [ + 'datatype' => 'integer', + ], 'offset' => [ 'datatype' => 'integer', ], @@ -1776,12 +1782,12 @@ private function checkParamDatatype($paramName, &$value, array $paramDef) { } } elseif ($datatype === 'boolean') { - if (!preg_match('/^(true|false)$/', $trimmed)) { + if (!preg_match('/^(true|false|t|f)$/', $trimmed)) { RestObjects::$apiResponse->fail('Bad request', 400, "Invalid boolean for $paramName parameter, value should be true or false"); } // Set the value to a real bool. - $value = $trimmed === 'true'; + $value = $trimmed === 'true' || $trimmed === 't'; } // If a limited options set available then check the value is in the list. if (!empty($paramDef['options']) && !in_array($trimmed, $paramDef['options'])) { From 6e62973a41206d415482b590dfbe3dcbd63153aa Mon Sep 17 00:00:00 2001 From: John van Breda Date: Thu, 25 Aug 2022 15:57:49 +0100 Subject: [PATCH 3/9] Documentation for autofeed and max_time For REST Api reports that link to Logstash --- modules/rest_api/i18n/en_GB/rest_api.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/modules/rest_api/i18n/en_GB/rest_api.php b/modules/rest_api/i18n/en_GB/rest_api.php index ff231840cb..d107d43bf5 100644 --- a/modules/rest_api/i18n/en_GB/rest_api.php +++ b/modules/rest_api/i18n/en_GB/rest_api.php @@ -667,7 +667,11 @@ Retrieves the contents of the folder specified by {path} of the reports directory on the warehouse. URL. TXT; -$lang['resources']['GET reports/{path}/{file-xml}'] = 'Access the output for a report specified by the supplied path.'; +$lang['resources']['GET reports/{path}/{file-xml}'] = << Date: Thu, 25 Aug 2022 16:03:26 +0100 Subject: [PATCH 4/9] Linting --- .../rest_api/libraries/RestApiResponse.php | 90 ++++++++++++------- 1 file changed, 58 insertions(+), 32 deletions(-) diff --git a/modules/rest_api/libraries/RestApiResponse.php b/modules/rest_api/libraries/RestApiResponse.php index e004083eca..bfddc043c8 100644 --- a/modules/rest_api/libraries/RestApiResponse.php +++ b/modules/rest_api/libraries/RestApiResponse.php @@ -19,13 +19,18 @@ * @link https://github.com/indicia-team/warehouse/ */ +/** + * Class which manages responses from the REST API. + */ class RestApiResponse { private $startTime; /** - * A template to define the header of any HTML pages output. Replace - * {{ base }} with the root path of the warehouse. + * A template to define the header of any HTML pages output. + * + * Replace {{ base }} with the root path of the warehouse. + * * @var string */ private $htmlHeader = <<<'HTML' @@ -43,6 +48,13 @@ class RestApiResponse {

HTML; + /** + * A template to define the footer of any HTML pages output. + * + * Replace {{ base }} with the root path of the warehouse. + * + * @var string + */ private $htmlFooter = <<<'HTML'
@@ -64,14 +76,14 @@ class RestApiResponse { * * @var bool */ - public $wantIndex = false; + public $wantIndex = FALSE; /** * Include empty output cells in HTML? * * @var bool */ - public $includeEmptyValues = true; + public $includeEmptyValues = TRUE; /** * Index method which provides top level help for the API resource endpoints. @@ -98,7 +110,7 @@ public function index(array $resourceConfig) { * Configuration for the list of available resources and the methods they * support. */ - private function indexHtml($resourceConfig) { + private function indexHtml(array $resourceConfig) { // Output an HTML page header. echo str_replace('{{ base }}', url::base(), $this->htmlHeader); $lang = [ @@ -147,7 +159,7 @@ private function indexHtml($resourceConfig) { elseif ($value === TRUE) { $key .= 'false'; } - else { + else { $key .= json_encode($value); } $optionTexts[] = '
  • ' . kohana::lang($key) . '
  • '; @@ -405,6 +417,7 @@ public function getUrlWithCurrentParams($url) { /** * Echos a successful response in HTML format. + * * @param array $data * @param array $options */ @@ -418,11 +431,11 @@ private function succeedHtml($data, $options) { $this->outputArrayAsHtml($options['metadata']); } - // output an index table if present for this output + // Output an index table if present for this output. if ($this->wantIndex && isset($data['data'])) { echo $this->getIndexAsHtml($data['data']); } - // output the main response body + // Output the main response body. if (isset($options['metadata']) || !empty($this->responseTitle)) { echo '

    Response

    '; } @@ -432,14 +445,19 @@ private function succeedHtml($data, $options) { elseif (is_object($data)) { $options['preprocess'] = true; // We are returning a single row from the database. - $this->outputResultAsHtml(array($data), $options); + $this->outputResultAsHtml([$data], $options); } echo str_replace('{{ base }}', url::base(), $this->htmlFooter); } /** - * For some resources when output as HTML, we insert an index into the top of the page. - * @return string HTML for the index. + * Insert an HTML index. + * + * For some resources when output as HTML, we insert an index into the top of + * the page. + * + * @return string + * HTML for the index. */ private function getIndexAsHtml($data) { $r = ''; @@ -472,13 +490,15 @@ private function getIndexAsHtml($data) { } /** - * Dumps out a nested array as a nested HTML table. Used to output response data when the - * format type requested is HTML. + * Dumps out a nested array as a nested HTML table. + * + * Used to output response data when the format type requested is HTML. * - * @param array $array Data to output + * @param array $array + * Data to output * @param array $options */ - private function outputArrayAsHtml($array, $options = array()) { + private function outputArrayAsHtml($array, $options = []) { if (count($array)) { $id = isset($options['tableId']) ? " id=\"$options[tableId]\"" : ''; echo ""; @@ -490,36 +510,39 @@ private function outputArrayAsHtml($array, $options = array()) { echo ""; } $keys = array_keys($array); - $col1 = is_integer($keys[0]) ? 'Row' : 'Field'; - $col2 = is_integer($keys[0]) ? 'Record' : 'Value'; + $col1 = is_int($keys[0]) ? 'Row' : 'Field'; + $col2 = is_int($keys[0]) ? 'Record' : 'Value'; $this->preProcessRow($array, $options); echo ""; echo ''; - foreach ($array as $key=>$value) { - if (empty($value) && !$this->includeEmptyValues) + foreach ($array as $key => $value) { + if (empty($value) && !$this->includeEmptyValues) { continue; - // If in a simple list of data or pg output, start preprocessing rows. Other structural output elements are not - // preprocessed. + } + // If in a simple list of data or pg output, start preprocessing rows. + // Other structural output elements are not preprocessed. $options['preprocess'] = is_int($key) || is_object($value); $class = is_array($value) && !empty($value['type']) ? " class=\"type-$value[type]\"" : ''; echo "
    $label
    $col1$col2
    $key"; $options['tableId'] = $key; - // Object data here means a pg result object. Or, if it is an non-associative array so a simple list, then - // output as a table rather than a nested structure. + // Object data here means a pg result object. Or, if it is an + // non-associative array so a simple list, then output as a table + // rather than a nested structure. if (is_object($value) || (is_array($value) && count($value) > 0 && is_int(array_keys($value)[0]))) { // recurse into pg result data $this->outputResultAsHtml($value, $options); } elseif (is_array($value)) { - // recurse into plain array data + // Recurse into plain array data. $this->outputArrayAsHtml($value, $options); } else { - // a simple value to output. If it contains an internal link then process it to hide user/secret data. + // A simple value to output. If it contains an internal link then + // process it to hide user/secret data. if (preg_match('/^http(s)?:\/\//', $value)) { $parts = explode('?', $value); $displayUrl = $parts[0]; - if (count($parts)>1) { + if (count($parts) > 1) { parse_str($parts[1], $params); unset($params['user']); unset($params['user_id']); @@ -541,21 +564,24 @@ private function outputArrayAsHtml($array, $options = array()) { /** * Dumps out an HTML table containing results from a PostgreSQL query. - * @param array $data PG result data to iterate through. - * @param array $options Options array. If this has a columns element, it is used to generate a header row and control - * the output. + * + * @param array $data + * PG result data to iterate through. + * @param array $options + * Options array. If this has a columns element, it is used to generate a + * header row and control the output. */ - private function outputResultAsHtml($data, $options) { + private function outputResultAsHtml(array $data, array $options) { echo ''; if (isset($options['columns'])) { // Ensure href and foriegn key column titles are added if we are including either of them. That's because these // are dynamically added to the data for each row as we go. if (!empty($options['preprocess'])) { if (!empty($options['attachHref']) && !in_array('href', $options['columns'])) { - $options['columns']['href'] = array(); + $options['columns']['href'] = []; } if (!empty($options['attachFkLink']) && !in_array($options['attachFkLink'][0], $options['columns'])) { - $options['columns'][$options['attachFkLink'][0]] = array(); + $options['columns'][$options['attachFkLink'][0]] = []; } } echo ''; From 0cd491a0d1f6d3853f04008363698a85c6fa7f90 Mon Sep 17 00:00:00 2001 From: John van Breda Date: Tue, 30 Aug 2022 10:52:37 +0100 Subject: [PATCH 5/9] Consistency of details page attribute formatting Includes option to format newlines and html links embedded in text. --- application/config/version.php | 4 +- .../202208291518_format_attr_value.sql | 37 +++++++++++++++++++ .../taxa/taxon_attributes_with_hiddens.xml | 15 ++++---- .../location_data_attributes_with_hiddens.xml | 6 +-- .../record_data_attributes_with_hiddens.xml | 10 ++--- .../sample_data_attributes_with_hiddens.xml | 5 ++- .../species_attr_description.xml | 24 ++++-------- 7 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 modules/indicia_setup/db/version_8_5_0/202208291518_format_attr_value.sql diff --git a/application/config/version.php b/application/config/version.php index 6ee6769bea..681a0a5850 100644 --- a/application/config/version.php +++ b/application/config/version.php @@ -29,14 +29,14 @@ * * @var string */ -$config['version'] = '8.4.2'; +$config['version'] = '8.5.0'; /** * Version release date. * * @var string */ -$config['release_date'] = '2022-08-24'; +$config['release_date'] = '2022-08-29'; /** * Link to the code repository downloads page. diff --git a/modules/indicia_setup/db/version_8_5_0/202208291518_format_attr_value.sql b/modules/indicia_setup/db/version_8_5_0/202208291518_format_attr_value.sql new file mode 100644 index 0000000000..9ef42e31f4 --- /dev/null +++ b/modules/indicia_setup/db/version_8_5_0/202208291518_format_attr_value.sql @@ -0,0 +1,37 @@ +CREATE OR REPLACE FUNCTION get_formatted_attr_text_value( + caption text, + value text, + output_formatting bool +) +RETURNS text +LANGUAGE 'plpgsql' +IMMUTABLE +AS +$BODY$ +--DECLARE +BEGIN + RETURN + CASE + WHEN substring(caption from (char_length(caption)-5) for 4) = 'link' AND substring(value from 0 for 4) = 'http' THEN + '' || value || '' + -- Colour value with a secondary colour. + WHEN value LIKE '#%;%' THEN ' ' + || ' ' + -- Single colour value. + WHEN value LIKE '#%' THEN ' ' + ELSE + CASE output_formatting + -- Newlines + WHEN 't' THEN + replace( + -- Embedded links in text block formatted if param set. + regexp_replace(value, '(http[^\s]*)', '\1'), + CHR(13), + '
    ' + ) + ELSE + value + END + END; +END +$BODY$; \ No newline at end of file diff --git a/reports/library/taxa/taxon_attributes_with_hiddens.xml b/reports/library/taxa/taxon_attributes_with_hiddens.xml index 0fe4a5c725..d702c200ca 100644 --- a/reports/library/taxa/taxon_attributes_with_hiddens.xml +++ b/reports/library/taxa/taxon_attributes_with_hiddens.xml @@ -3,7 +3,7 @@ description="Report used to retrieve custom attributes of an species which are not included in a list of attributes to ignore." > - select + select distinct ttl.id, 'Record' as attribute_type, a.system_function, CASE a.data_type WHEN 'T'::bpchar THEN 'Text'::bpchar @@ -14,9 +14,9 @@ WHEN 'D'::bpchar THEN 'Specific Date'::bpchar WHEN 'V'::bpchar THEN 'Vague Date'::bpchar ELSE a.data_type - END AS data_type, a.caption, + END AS data_type, a.caption, CASE a.data_type - WHEN 'T'::bpchar THEN av.text_value + WHEN 'T'::bpchar THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L'::bpchar THEN lookup.term::text WHEN 'I'::bpchar THEN av.int_value::character varying::text WHEN 'B'::bpchar THEN av.int_value::character varying::text @@ -24,7 +24,7 @@ WHEN 'D'::bpchar THEN av.date_start_value::character varying::text WHEN 'V'::bpchar THEN (av.date_start_value::character varying::text || ' - '::text) || av.date_end_value::character varying::text ELSE NULL::text - END AS value, + END AS value, CASE a.data_type WHEN 'T'::bpchar THEN av.text_value WHEN 'L'::bpchar THEN av.int_value::character varying::text @@ -39,7 +39,7 @@ join taxa_taxon_lists ttl2 on ttl2.taxon_meaning_id=ttl.taxon_meaning_id and ttl2.deleted=false join taxa_taxon_list_attribute_values av on av.taxa_taxon_list_id=ttl2.id and av.deleted=false join taxa_taxon_list_attributes a on a.id=av.taxa_taxon_list_attribute_id and a.deleted=false - left join cache_termlists_terms lookup on lookup.id=av.int_value + left join cache_termlists_terms lookup on lookup.id=av.int_value WHERE ttl.id=#taxa_taxon_list_id# AND ttl.deleted=false @@ -48,9 +48,10 @@ - - + diff --git a/reports/reports_for_prebuilt_forms/location_details/location_data_attributes_with_hiddens.xml b/reports/reports_for_prebuilt_forms/location_details/location_data_attributes_with_hiddens.xml index 3eb62da482..f906594a39 100644 --- a/reports/reports_for_prebuilt_forms/location_details/location_data_attributes_with_hiddens.xml +++ b/reports/reports_for_prebuilt_forms/location_details/location_data_attributes_with_hiddens.xml @@ -17,10 +17,7 @@ END AS data_type, CASE '#language#' WHEN '' THEN a.caption ELSE COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->'#language#')::varchar), a.caption) END AS caption, STRING_AGG(CASE a.data_type - WHEN 'T' THEN CASE WHEN substring(a.caption from (char_length(a.caption)-5) for 4) = 'link' AND substring(av.text_value from 0 for 4) = 'http' THEN - '<a href="' || av.text_value || '">' || av.text_value || '</a>' - ELSE - av.text_value END + WHEN 'T' THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L' THEN lookup.term::text WHEN 'I' THEN av.int_value::varchar WHEN 'B' THEN av.int_value::varchar @@ -65,5 +62,6 @@ datatype='lookup' lookup_values='in:Include,not in:Exclude' /> + diff --git a/reports/reports_for_prebuilt_forms/record_details_2/record_data_attributes_with_hiddens.xml b/reports/reports_for_prebuilt_forms/record_details_2/record_data_attributes_with_hiddens.xml index 490fe4c3f4..2698ddde31 100644 --- a/reports/reports_for_prebuilt_forms/record_details_2/record_data_attributes_with_hiddens.xml +++ b/reports/reports_for_prebuilt_forms/record_details_2/record_data_attributes_with_hiddens.xml @@ -17,10 +17,7 @@ END AS data_type, CASE '#language#' WHEN '' THEN a.caption ELSE COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->'#language#')::varchar), a.caption) END AS caption, CASE a.data_type - WHEN 'T' THEN CASE WHEN substring(a.caption from (char_length(a.caption)-5) for 4) = 'link' AND substring(av.text_value from 0 for 4) = 'http' THEN - '<a href="' || av.text_value || '">' || av.text_value || '</a>' - ELSE - av.text_value END + WHEN 'T' THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L' THEN lookup.term::text WHEN 'I' THEN av.int_value::varchar WHEN 'B' THEN av.int_value::varchar @@ -66,7 +63,7 @@ END AS data_type, CASE '#language#' WHEN '' THEN a.caption ELSE COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->'#language#')::varchar), a.caption) END AS caption, CASE a.data_type - WHEN 'T' THEN av.text_value + WHEN 'T' THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L' THEN lookup.term::text WHEN 'I' THEN coalesce(l.name, av.int_value::varchar) WHEN 'B' THEN av.int_value::varchar @@ -115,7 +112,7 @@ END AS data_type, CASE '#language#' WHEN '' THEN a.caption ELSE COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->'#language#')::varchar), a.caption) END AS caption, CASE a.data_type - WHEN 'T' THEN av.text_value + WHEN 'T' THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L' THEN lookup.term::text WHEN 'I' THEN coalesce(l.name, av.int_value::varchar) WHEN 'B' THEN av.int_value::varchar @@ -160,5 +157,6 @@ datatype='lookup' lookup_values='in:Include,not in:Exclude' /> + diff --git a/reports/reports_for_prebuilt_forms/sample_details/sample_data_attributes_with_hiddens.xml b/reports/reports_for_prebuilt_forms/sample_details/sample_data_attributes_with_hiddens.xml index 1c6bc47537..f66d7cb159 100644 --- a/reports/reports_for_prebuilt_forms/sample_details/sample_data_attributes_with_hiddens.xml +++ b/reports/reports_for_prebuilt_forms/sample_details/sample_data_attributes_with_hiddens.xml @@ -18,7 +18,7 @@ END AS data_type, CASE '#language#' WHEN '' THEN a.caption ELSE COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->'#language#')::varchar), a.caption) END AS caption, CASE a.data_type - WHEN 'T' THEN av.text_value + WHEN 'T' THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L' THEN lookup.term::text WHEN 'I' THEN coalesce(l.name, av.int_value::varchar) WHEN 'B' THEN av.int_value::varchar @@ -65,7 +65,7 @@ END AS data_type, CASE '#language#' WHEN '' THEN a.caption ELSE COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->'#language#')::varchar), a.caption) END AS caption, CASE a.data_type - WHEN 'T' THEN av.text_value + WHEN 'T' THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L' THEN lookup.term::text WHEN 'I' THEN coalesce(l.name, av.int_value::varchar) WHEN 'B' THEN av.int_value::varchar @@ -109,5 +109,6 @@ datatype='lookup' lookup_values='in:Include,not in:Exclude' /> + diff --git a/reports/reports_for_prebuilt_forms/species_details/species_attr_description.xml b/reports/reports_for_prebuilt_forms/species_details/species_attr_description.xml index 913a0b06f2..f37469f93c 100644 --- a/reports/reports_for_prebuilt_forms/species_details/species_attr_description.xml +++ b/reports/reports_for_prebuilt_forms/species_details/species_attr_description.xml @@ -14,28 +14,19 @@ FROM ( COALESCE(subcat_l.term, subcat.term) as subcategory, CASE '#include_captions#' WHEN '1' THEN trim(regexp_replace(regexp_replace( - CASE '#language#' WHEN '' THEN - '<b>' || a.caption || '</b>' - ELSE - '<b>' || COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->>'#language#')::varchar), a.caption) || '</b>' + CASE '#language#' WHEN '' THEN + '<b>' || a.caption || '</b>' + ELSE + '<b>' || COALESCE(TRIM(BOTH '"' FROM (a.caption_i18n->>'#language#')::varchar), a.caption) || '</b>' END, ' \(95%\)$', ''), '^' || COALESCE(subcat_l.term, subcat.term), '')) || ': ' - ELSE - '' + ELSE + '' END as caption, a.caption as raw_caption, tlttla.weight, CASE a.data_type - WHEN 'T'::bpchar THEN - CASE - -- colour value with a secondary colour. - WHEN av.text_value LIKE '#%;%' THEN '<span style="width: 30px; height: 15px; display: inline-block; background-color: ' || split_part(av.text_value, ';', 1) || '"> </span>' - || '<span style="width: 30px; height: 15px; display: inline-block; background-color: ' || split_part(av.text_value, ';', 2) || '"> </span>' - -- single colour value. - WHEN av.text_value LIKE '#%' THEN '<span style="width: 30px; height: 15px; display: inline-block; background-color: ' || av.text_value || '"> </span>' - -- any other value. - ELSE av.text_value - END + WHEN 'T' THEN get_formatted_attr_text_value(a.caption, av.text_value, '#output_formatting#') WHEN 'L'::bpchar THEN COALESCE(t_l.term::text, t.term::text) WHEN 'I'::bpchar THEN CASE @@ -106,6 +97,7 @@ FROM ( + From 55f8191d5d747af6111ba0cc0eee6b9bb6016990 Mon Sep 17 00:00:00 2001 From: John van Breda Date: Thu, 8 Sep 2022 10:58:14 +0100 Subject: [PATCH 6/9] Linting --- application/libraries/XMLReportReader.php | 203 +++++++++++----------- 1 file changed, 106 insertions(+), 97 deletions(-) diff --git a/application/libraries/XMLReportReader.php b/application/libraries/XMLReportReader.php index 7623a58265..81b89ccfe2 100644 --- a/application/libraries/XMLReportReader.php +++ b/application/libraries/XMLReportReader.php @@ -848,7 +848,7 @@ public function describeReport($descLevel) { /** */ public function getAttributeDefns() { - return $this->attributes; + return $this->attributes; } public function getVagueDateProcessing() { @@ -1005,7 +1005,7 @@ private function mergeParam($name, $reader = NULL) { 'population_call', 'linked_to', 'linked_filter_field', - 'order_by' + 'order_by', ]); if ($this->params[$name]['datatype'] === 'lookup') { @@ -1030,21 +1030,21 @@ private function mergeParam($name, $reader = NULL) { if (!isset($this->params[$name]['joins'])) { $this->params[$name]['joins'] = []; } - $this->params[$name]['joins'][] = array( + $this->params[$name]['joins'][] = [ 'value' => $reader->getAttribute('value'), 'operator' => $reader->getAttribute('operator'), 'sql' => $reader->readString(), - ); + ]; } if ($reader->nodeType == XMLREADER::ELEMENT && $reader->name === 'where') { if (!isset($this->params[$name]['wheres'])) { $this->params[$name]['wheres'] = []; } - $this->params[$name]['wheres'][] = array( + $this->params[$name]['wheres'][] = [ 'value' => $reader->getAttribute('value'), 'operator' => $reader->getAttribute('operator'), 'sql' => $reader->readString(), - ); + ]; } } } @@ -1238,8 +1238,7 @@ private function mergeXmlColumn($reader) { private function mergeColumn($name, $display = '', $style = '', $feature_style='', $class='', $visible='', $img='', $orderby='', $mappable='', $autodef=TRUE) { - if (array_key_exists($name, $this->columns)) - { + if (array_key_exists($name, $this->columns)) { if ($display != '') $this->columns[$name]['display'] = $display; if ($style != '') $this->columns[$name]['style'] = $style; if ($feature_style != '') $this->columns[$name]['feature_style'] = $feature_style; @@ -1256,17 +1255,17 @@ private function mergeColumn($name, $display = '', $style = '', $feature_style=' } else { - $this->columns[$name] = array( - 'display' => $display, - 'style' => $style, - 'feature_style' => $feature_style, - 'class' => $class, - 'visible' => $visible == '' ? 'true' : $visible, - 'img' => $img == '' ? 'false' : $img, - 'orderby' => $orderby, - 'mappable' => empty($mappable) ? 'false' : $mappable, - 'autodef' => $autodef, - ); + $this->columns[$name] = [ + 'display' => $display, + 'style' => $style, + 'feature_style' => $feature_style, + 'class' => $class, + 'visible' => $visible == '' ? 'true' : $visible, + 'img' => $img == '' ? 'false' : $img, + 'orderby' => $orderby, + 'mappable' => empty($mappable) ? 'false' : $mappable, + 'autodef' => $autodef, + ]; } } @@ -1275,51 +1274,55 @@ private function setTable($tablename, $where) { $this->tableIndex = 0; $this->nextTableIndex = 1; $this->tables[$this->tableIndex] = [ - 'tablename' => $tablename, - 'parent' => -1, - 'parentKey' => '', - 'tableKey' => '', - 'join' => '', - 'attributes' => '', - 'where' => $where, - 'columns' => [] + 'tablename' => $tablename, + 'parent' => -1, + 'parentKey' => '', + 'tableKey' => '', + 'join' => '', + 'attributes' => '', + 'where' => $where, + 'columns' => [], ]; } private function setSubTable($tablename, $parentKey, $tableKey, $join, $where) { - if($tableKey == ''){ - if($parentKey == 'id'){ - $tableKey = 'lt'.$this->nextTableIndex.".".(inflector::singular($this->tables[$this->tableIndex]['tablename'])).'_id'; + if ($tableKey == '') { + if ($parentKey == 'id') { + $tableKey = 'lt' . $this->nextTableIndex . "." . (inflector::singular($this->tables[$this->tableIndex]['tablename'])) . '_id'; } else { - $tableKey = 'lt'.$this->nextTableIndex.'.id'; + $tableKey = 'lt' . $this->nextTableIndex . '.id'; } } else { - $tableKey = 'lt'.$this->nextTableIndex.".".$tableKey; + $tableKey = 'lt' . $this->nextTableIndex . "." . $tableKey; + } + if ($parentKey == '') { + $parentKey = 'lt' . $this->tableIndex . "." . (inflector::singular($tablename)) . '_id'; } - if($parentKey == ''){ - $parentKey = 'lt'.$this->tableIndex.".".(inflector::singular($tablename)).'_id'; - } else { // force the link as this table has foreign key to parent table, standard naming convention. - $parentKey = 'lt'.$this->tableIndex.".".$parentKey; + else { + // Force the link as this table has foreign key to parent table, standard + // naming convention. + $parentKey = 'lt' . $this->tableIndex . '.' . $parentKey; } - $this->tables[$this->nextTableIndex] = array( - 'tablename' => $tablename, - 'parent' => $this->tableIndex, - 'parentKey' => $parentKey, - 'tableKey' => $tableKey, - 'join' => $join, - 'attributes' => '', - 'where' => $where, - 'columns' => []); - $this->tableIndex=$this->nextTableIndex; + $this->tables[$this->nextTableIndex] = [ + 'tablename' => $tablename, + 'parent' => $this->tableIndex, + 'parentKey' => $parentKey, + 'tableKey' => $tableKey, + 'join' => $join, + 'attributes' => '', + 'where' => $where, + 'columns' => [], + ]; + $this->tableIndex = $this->nextTableIndex; $this->nextTableIndex++; } - private function mergeTabColumn($name, $func = '', $display = '', $style = '', $feature_style = '', $class='', $visible='', $autodef=FALSE) { + private function mergeTabColumn($name, $func = '', $display = '', $style = '', $feature_style = '', $class = '', $visible = '', $autodef = FALSE) { $found = FALSE; - for($r = 0; $r < count($this->tables[$this->tableIndex]['columns']); $r++){ - if($this->tables[$this->tableIndex]['columns'][$r]['name'] == $name) { + for ($r = 0; $r < count($this->tables[$this->tableIndex]['columns']); $r++){ + if ($this->tables[$this->tableIndex]['columns'][$r]['name'] == $name) { $found = TRUE; - if($func != '') { + if ($func != '') { $this->tables[$this->tableIndex]['columns'][$r]['func'] = $func; } } @@ -1329,7 +1332,7 @@ private function mergeTabColumn($name, $func = '', $display = '', $style = '', $ 'name' => $name, 'func' => $func, ]; - if($display == '') { + if ($display == '') { $display = $this->tables[$this->tableIndex]['tablename']." ".$name; } } @@ -1371,7 +1374,7 @@ private function setAttributes($where, $separator, $hideVagueDateFields, $meanin $thisDefn->id = 'id'; // id is the name of the column in the subquery holding the attribute id. $thisDefn->separator = $separator; $thisDefn->hideVagueDateFields = $hideVagueDateFields; - $thisDefn->columnPrefix = 'attr_' . $this->tableIndex.'_'; + $thisDefn->columnPrefix = 'attr_' . $this->tableIndex . '_'; // Folowing is used the query builder only. $thisDefn->parentTableIndex = $this->tableIndex; $thisDefn->where = $where; @@ -1392,14 +1395,13 @@ private function setDownload($mode) { * matching from. Commas that are part of nested selects or function calls are ignored * provided they are enclosed in brackets. */ - private function inferFromQuery() - { - // Find the columns we're searching for - nested between a SELECT and a FROM. - // To ensure we can detect the words FROM, SELECT and AS, use a regex to wrap - // spaces around them, then can do a regular string search - $this->query=preg_replace("/\b(select)\b/i", ' select ', $this->query); - $this->query=preg_replace("/\b(from)\b/i", ' from ', $this->query); - $this->query=preg_replace("/\b(as)\b/i", ' as ', $this->query); + private function inferFromQuery() { + // Find the columns we're searching for - nested between a SELECT and a + // FROM. To ensure we can detect the words FROM, SELECT and AS, use a regex + // to wrap spaces around them, then can do a regular string search. + $this->query = preg_replace("/\b(select)\b/i", ' select ', $this->query); + $this->query = preg_replace("/\b(from)\b/i", ' from ', $this->query); + $this->query = preg_replace("/\b(as)\b/i", ' as ', $this->query); $i0 = strpos($this->query, ' select ') + 7; $nesting = 1; $offset = $i0; @@ -1407,13 +1409,13 @@ private function inferFromQuery() $nextSelect = strpos($this->query, ' select ', $offset); $nextFrom = strpos($this->query, ' from ', $offset); if ($nextSelect !== FALSE && $nextSelect < $nextFrom) { - //found start of sub-query + // Found start of sub-query. $nesting++; $offset = $nextSelect + 7; } else { $nesting--; if ($nesting != 0) { - //found end of sub-query + // Found end of sub-query. $offset = $nextFrom + 5; } } @@ -1425,36 +1427,36 @@ private function inferFromQuery() $colString = str_replace('#fields#', '', substr($this->query, $i0, $i1)); // Now divide up the list of columns, which are comma separated, but ignore - // commas nested in brackets + // commas nested in brackets. $colStart = 0; - $nextComma = strpos($colString, ',', $colStart); - while ($nextComma !== FALSE) - {//loop through columns - $nextOpen = strpos($colString, '(', $colStart); - while ($nextOpen !== FALSE && $nextComma !==FALSE && $nextOpen < $nextComma) - { //skipping commas in brackets + $nextComma = strpos($colString, ',', $colStart); + while ($nextComma !== FALSE) { + // Loop through columns. + $nextOpen = strpos($colString, '(', $colStart); + while ($nextOpen !== FALSE && $nextComma !== FALSE && $nextOpen < $nextComma) { + // Skipping commas in brackets. $offset = $this->strposclose($colString, $nextOpen) + 1; - $nextComma = strpos($colString, ',', $offset); - $nextOpen = strpos($colString, '(', $offset); + $nextComma = strpos($colString, ',', $offset); + $nextOpen = strpos($colString, '(', $offset); } if ($nextComma !== FALSE) { - //extract column and move on to next + // Extract column and move on to next. $cols[] = substr($colString, $colStart, ($nextComma - $colStart)); $colStart = $nextComma + 1; - $nextComma = strpos($colString, ',', $colStart); - } + $nextComma = strpos($colString, ',', $colStart); + } } - //extract final column + // Extract final column. $cols[] = substr($colString, $colStart); - // We have cols, which may either be of the form 'x', 'table.x' or 'x as y'. Either way the column name is the part after the last - // space and full stop. - foreach ($cols as $col) - { - // break down by spaces - $b = explode(' ' , trim($col)); - // break down the part after the last space, by - $c = explode('.' , array_pop($b)); + // We have cols, which may either be of the form 'x', 'table.x' or 'x as + // y'. Either way the column name is the part after the last space and full + // stop. + foreach ($cols as $col) { + // Break down by spaces. + $b = explode(' ', trim($col)); + // Break down the part after the last space. + $c = explode('.', array_pop($b)); $d = array_pop($c); $this->mergeColumn(trim($d)); } @@ -1462,34 +1464,41 @@ private function inferFromQuery() // Okay, now we need to find parameters, which we do with regex. preg_match_all('/#([a-z0-9_]+)#%/i', $this->query, $matches); // Here is why I remember (yet again) why I hate PHP... - foreach ($matches[1] as $param) - { + foreach ($matches[1] as $param) { $this->mergeParam($param); } } /** - * Returns the numeric position of the closing bracket matching the opening bracket - * @param $haystack The string to search - * @param $open The numeric position of the opening bracket - * @return The numeric position of the closing bracket or FALSE if not present + * Returns numeric pos of the closing bracket matching an opening bracket. + * + * @param string $haystack + * The string to search. + * @param int $open + * The numeric position of the opening bracket. + * + * @return int + * The numeric position of the closing bracket or FALSE if not present. */ private function strposclose($haystack, $open) { $nesting = 1; $offset = $open + 1; do { - $nextOpen = strpos($haystack, '(', $offset); - $nextClose = strpos($haystack, ')', $offset); - if ($nextClose === FALSE) return FALSE; + $nextOpen = strpos($haystack, '(', $offset); + $nextClose = strpos($haystack, ')', $offset); + if ($nextClose === FALSE) { + return FALSE; + } if ($nextOpen !== FALSE and $nextOpen < $nextClose) { $nesting++; $offset = $nextOpen + 1; - } else { + } + else { $nesting--; $offset = $nextClose + 1; } - } - while ($nesting > 0); - return $offset -1; + } while ($nesting > 0); + return $offset - 1; } -} \ No newline at end of file + +} From 9d02873d9581af5eda073cd7145d463f823e3b45 Mon Sep 17 00:00:00 2001 From: John van Breda Date: Fri, 9 Sep 2022 09:08:37 +0100 Subject: [PATCH 7/9] Version up for code already in develop due to hotfeature --- application/config/version.php | 2 +- .../202208291518_format_attr_value.sql | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename modules/indicia_setup/db/{version_8_5_0 => version_8_6_0}/202208291518_format_attr_value.sql (100%) diff --git a/application/config/version.php b/application/config/version.php index a95e5ecb50..1e4be2c762 100644 --- a/application/config/version.php +++ b/application/config/version.php @@ -29,7 +29,7 @@ * * @var string */ -$config['version'] = '8.5.0'; +$config['version'] = '8.6.0'; /** * Version release date. diff --git a/modules/indicia_setup/db/version_8_5_0/202208291518_format_attr_value.sql b/modules/indicia_setup/db/version_8_6_0/202208291518_format_attr_value.sql similarity index 100% rename from modules/indicia_setup/db/version_8_5_0/202208291518_format_attr_value.sql rename to modules/indicia_setup/db/version_8_6_0/202208291518_format_attr_value.sql From 4f227aef77a7c99e9f0588eb9495dc5a0e42221c Mon Sep 17 00:00:00 2001 From: John van Breda Date: Fri, 9 Sep 2022 11:31:36 +0100 Subject: [PATCH 8/9] Changelog updated --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5da80db8..5f7b523735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Version 8.5.0 +*2022-09-09* + +* Adds a new standard filter parameter for filtering occurrences by sample ID (smp_id). +* Adds reports required to support a new recording_system_links Drupal module. + # Version 8.4.0 *2022-08-10* From 5d74ca01cb563af7dabeaf77fc06f30df7f0a62b Mon Sep 17 00:00:00 2001 From: John van Breda Date: Fri, 9 Sep 2022 12:15:49 +0100 Subject: [PATCH 9/9] CHANGELOG updated --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7b523735..15354d1dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Version 8.6.0 +*2022-09-09" + +* Support for new output_formatting option in reports for details pages (occurrences, samples, + locations) with auto-formatting of hyperlinks for text attribute data. +* Improvements to the REST API's auto-generated documentation. + # Version 8.5.0 *2022-09-09*