From 891c67b4298f6497e525df9137592094eb83f004 Mon Sep 17 00:00:00 2001
From: "alex.huang"
Date: Wed, 31 Mar 2021 16:33:39 +1100
Subject: [PATCH 01/51] Release/3.0.3 (#433)
* Feature/duplicate record (#429)
Add duplicate assertion type to flag an issue
* enhanced duplicate record flag (#431)
* release 3.0.3
---
build.gradle | 2 +-
grails-app/assets/javascripts/show.js | 194 +++++++++++++++++-
.../biocache/hubs/AssertionsController.groovy | 13 +-
.../biocache/hubs/OccurrenceController.groovy | 20 ++
grails-app/i18n/messages_en.properties | 25 +++
.../biocache/hubs/WebServicesService.groovy | 18 +-
.../views/occurrence/_recordSidebar.gsp | 50 ++++-
grails-app/views/occurrence/show.gsp | 22 ++
8 files changed, 327 insertions(+), 17 deletions(-)
diff --git a/build.gradle b/build.gradle
index a9b99c8cf..4b1734ba9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ buildscript {
}
}
-version "3.0.2"
+version "3.0.3"
group "au.org.ala.plugins.grails"
apply plugin:"eclipse"
diff --git a/grails-app/assets/javascripts/show.js b/grails-app/assets/javascripts/show.js
index 34ea34393..e9a49dfc3 100644
--- a/grails-app/assets/javascripts/show.js
+++ b/grails-app/assets/javascripts/show.js
@@ -27,6 +27,121 @@ $(document).ready(function() {
$('.missingPropResult').toggle();
});
+ $('#copyRecordIdToClipboard').on('click', function(e) {
+ var copyText = document.querySelector("#hidden-uuid");
+ copyText.type = 'text';
+ copyText.select();
+ document.execCommand("copy");
+ copyText.type = 'hidden';
+ var $parent = $('#copyRecordIdToClipboard-parent');
+ $parent.tooltip('show');
+ setTimeout(function() {
+ $parent.tooltip('hide');
+ }, 1000);
+ // alert("Copied");
+ });
+ var recordIdValid = false;
+ function validateIssueForm() {
+ var issueCode = $('#issue').val();
+ var relatedRecordReason = $('#relatedRecordReason').val();
+ if (issueCode == '20020') {
+ return recordIdValid && relatedRecordReason;
+ }
+ return true;
+ }
+ function setIssueFormButtonState() {
+ $('#issueForm input[type=submit]').prop('disabled', !validateIssueForm());
+ }
+ $('#issue').on('change', function(e) {
+ var $this = $(this);
+ var val = $this.val();
+ var $submit = $('#issueForm input[type=submit]');
+ var $p = $('#related-record-p, #related-record-reason-p');
+ // if duplicate record
+ if (val === '20020') {
+ $('#relatedRecordId').val('');
+ recordIdValid = false;
+ $p.show();
+ } else {
+ $p.hide();
+ $('#related-record-id-not-found').hide();
+ $('#related-record-id-found').hide();
+ $('#related-record-id-loading').hide();
+ // hide the records table
+ $('#records_comparison_table').hide();
+ $('#records_comparison_heading').hide();
+ }
+ setIssueFormButtonState();
+ });
+
+ $('#relatedRecordReason').on('change', function(e) {
+ var col_reason = $('#col_duplicate_reason');
+ var relatedRecordReason = $('#relatedRecordReason').val();
+ if (relatedRecordReason === '') {
+ $(col_reason).text('');
+ } else {
+ $(col_reason).text(jQuery.i18n.prop('related.record.reason.description.' + relatedRecordReason));
+ }
+ setIssueFormButtonState();
+ })
+
+ $('#relatedRecordId').on('change', function(e) {
+ var $this = $(this);
+ var $submit = $('#issueForm input[type=submit]');
+ var val = $this.val().trim();
+ if (val === OCC_REC.recordUuid) {
+ alert("You can't mark this record as a duplicate of itself!");
+ recordIdValid = false;
+ } else if (val === '') {
+ $('#related-record-id-not-found').hide();
+ $('#related-record-id-found').hide();
+ $('#related-record-id-loading').hide();
+ $('#records_comparison_table').hide();
+ $('#records_comparison_heading').hide();
+ $('#relatedRecordReason').val("");
+ $('#col_duplicate_reason').text('');
+ recordIdValid = false;
+ } else {
+ $('#related-record-id-loading').show();
+ $.get( OCC_REC.contextPath + "/occurrence/exists/" + val).success(function(data) {
+ $('#related-record-id-loading').hide();
+ if (data.error) {
+ // show error
+ $('#related-record-id-not-found').text('More than 1 record found with specified id, please use a more specific id').show();
+ // hide compare table
+ $('#records_comparison_heading').hide();
+ $('#records_comparison_table').hide();
+ recordIdValid = false;
+ } else {
+ // hide error
+ $('#related-record-id-not-found').hide();
+ // show compare table
+ $('#related-record-id-found').show();
+ $('#records_comparison_table').show();
+ $('#records_comparison_heading').show();
+ // populate the table
+ $('#t_scientificName').text(data.scientificName ? data.scientificName : '');
+ $('#t_stateProvince').text(data.stateProvince ? data.stateProvince : '');
+ $('#t_decimalLongitude').text(data.decimalLongitude ? data.decimalLongitude : '');
+ $('#t_decimalLatitude').text(data.decimalLatitude ? data.decimalLatitude : '');
+ $('#t_eventDate').text(data.eventDate ? data.eventDate : '');
+ recordIdValid = true;
+ }
+ }).error(function () {
+ $('#related-record-id-not-found').text("The record id can't be found.").show();
+ $('#related-record-id-found').hide();
+ $('#related-record-id-loading').hide();
+ $('#records_comparison_table').hide();
+ $('#records_comparison_heading').hide();
+ $('#relatedRecordReason').val("");
+ $('#col_duplicate_reason').text('');
+ }).always(function() {
+ setIssueFormButtonState();
+ });
+ }
+ setIssueFormButtonState();
+ });
+
refreshUserAnnotations();
// bind to form submit for assertions
@@ -34,6 +149,8 @@ $(document).ready(function() {
e.preventDefault();
var comment = $("#issueComment").val();
var code = $("#issue").val();
+ var relatedRecordId = $('#relatedRecordId').val();
+ var relatedRecordReason = $('#relatedRecordReason').val();
var userDisplayName = OCC_REC.userDisplayName //'${userDisplayName}';
var recordUuid = OCC_REC.recordUuid //'${ala:escapeJS(record.raw.rowKey)}';
if(code!=""){
@@ -54,8 +171,16 @@ $(document).ready(function() {
if (bPreventAddingIssue) {
alert("You cannot flag an issue with the same type that has already been verified.");
return;
+ } else if (code == '20020' && !relatedRecordId) {
+ alert("You must provide a duplicate record id to mark this as a duplicate");
+ return;
+ } else if (code == '20020' && !relatedRecordReason) {
+ alert("You must select a reason to mark this record as a duplicate");
+ return;
+ } else if (code == '20020' && relatedRecordId == recordUuid) {
+ alert("You can't mark a record as a duplicate of itself");
+ return;
} else {
-
$.post(OCC_REC.contextPath + "/occurrences/assertions/add",
{
recordUuid: recordUuid,
@@ -63,13 +188,15 @@ $(document).ready(function() {
comment: comment,
userAssertionStatus: 'Open issue',
userId: OCC_REC.userId,
- userDisplayName: userDisplayName
+ userDisplayName: userDisplayName,
+ relatedRecordId: relatedRecordId,
+ relatedRecordReason: relatedRecordReason,
},
function (data) {
$('#assertionSubmitProgress').css({'display': 'none'});
$("#submitSuccess").html("Thanks for flagging the problem!");
$("#issueFormSubmit").hide();
- $("input:reset").hide();
+ $("input#cancel").hide();
$("input#close").show();
//retrieve all assertions
$.get(OCC_REC.contextPath + '/assertions/' + OCC_REC.recordUuid, function (data) { // recordUuid=${record.raw.uuid}
@@ -293,7 +420,6 @@ function getMessage(userAssertionCode) {
function refreshUserAnnotations(){
$.get( OCC_REC.contextPath + "/assertions/" + OCC_REC.recordUuid, function(data) {
-
if (data.assertionQueries.length == 0 && data.userAssertions.length == 0) {
$('#userAnnotationsDiv').hide('slow');
$('#userAssertionsContainer').hide("slow");
@@ -309,7 +435,9 @@ function refreshUserAnnotations(){
var $clone = $('#userAnnotationTemplate').clone();
$clone.find('.issue').text(data.assertionQueries[i].assertionType);
$clone.find('.user').text(data.assertionQueries[i].userName);
- $clone.find('.comment').text('Comment: ' + data.assertionQueries[i].comment);
+ if (data.assertionQueries[i].hasOwnProperty('comment')) {
+ $clone.find('.comment').text('Comment: ' + data.assertionQueries[i].comment);
+ }
$clone.find('.created').text('Date created: ' + (moment(data.assertionQueries[i].createdDate).format('YYYY-MM-DD')));
if(data.assertionQueries[i].recordCount > 1){
$clone.find('.viewMore').css({display:'block'});
@@ -332,10 +460,59 @@ function refreshUserAnnotations(){
$clone.prop('id', "userAnnotation_" + userAssertion.uuid);
$clone.find('.issue').text(jQuery.i18n.prop(userAssertion.name));
$clone.find('.user').text(userAssertion.userDisplayName);
- $clone.find('.comment').text('Comment: ' + userAssertion.comment);
+ if (userAssertion.hasOwnProperty('comment')) {
+ $clone.find('.comment').text('Comment: ' + userAssertion.comment);
+ }
$clone.find('.userRole').text(userAssertion.userRole != null ? userAssertion.userRole : '');
$clone.find('.userEntity').text(userAssertion.userEntityName != null ? userAssertion.userEntityName : '');
$clone.find('.created').text('Date created: ' + (moment(userAssertion.created, "YYYY-MM-DDTHH:mm:ssZ").format('YYYY-MM-DD HH:mm:ss')));
+ if (userAssertion.relatedRecordId) {
+ $clone.find('.related-record').show();
+ // show related record id
+ $clone.find('.related-record-id').show();
+ $clone.find('.related-record-id-span').text(userAssertion.relatedRecordId);
+ var href = $clone.find('.related-record-link').attr('href');
+ $clone.find('.related-record-link').attr('href', href.replace('replace-me', userAssertion.relatedRecordId));
+ if (userAssertion.code === 20020) {
+ $clone.find('.related-record-span-user-duplicate').show();
+
+ $.get( OCC_REC.contextPath + "/occurrence/exists/" + userAssertion.relatedRecordId).success(function(data) {
+ if (!data.error) {
+ if (data.scientificName) {
+ $clone.find('.related-record-name').show();
+ $clone.find('.related-record-name-span').text(data.scientificName);
+ }
+
+ if (data.stateProvince) {
+ $clone.find('.related-record-state').show();
+ $clone.find('.related-record-state-span').text(data.stateProvince);
+ }
+
+ if (data.decimalLongitude) {
+ $clone.find('.related-record-latitude').show();
+ $clone.find('.related-record-latitude-span').text(data.decimalLongitude);
+ }
+
+ if (data.decimalLatitude) {
+ $clone.find('.related-record-longitude').show();
+ $clone.find('.related-record-longitude-span').text(data.decimalLatitude);
+ }
+
+ if (data.eventDate) {
+ $clone.find('.related-record-eventdate').show();
+ $clone.find('.related-record-eventdate-span').text(data.eventDate);
+ }
+ }
+ })
+ } else {
+ $clone.find('.related-record-span-default').show();
+ }
+ }
+ if (userAssertion.relatedRecordReason) {
+ $clone.find('.related-record-reason').show();
+ $clone.find('.related-record-reason-span').text(jQuery.i18n.prop('related.record.reason.' + userAssertion.relatedRecordReason));
+ $clone.find('.related-record-reason-explanation').text(jQuery.i18n.prop('related.record.reason.explanation.' + userAssertion.relatedRecordReason)).show();
+ }
if (userAssertion.userRole != null) {
$clone.find('.userRole').text(', ' + userAssertion.userRole);
}
@@ -423,7 +600,7 @@ function deleteAssertionPrompt(event) {
var isConfirmed = confirm('Are you sure you want to delete this flagged issue?');
if (isConfirmed === true) {
$('#' + event.data.qa_uuid + ' .deleteAssertionSubmitProgress').css({'display':'inline'});
- console.log(event.data.qa_uuid);
+ //console.log(event.data.qa_uuid);
deleteAssertion(event.data.rec_uuid, event.data.qa_uuid);
}
}
@@ -481,8 +658,7 @@ function updateConfirmVerificationEvents(occUuid, assertionUuid, userDisplayName
return false;
}
- console.log("Submitting an assertion with userAssertionStatus: " + userAssertionStatus)
-
+ //console.log("Submitting an assertion with userAssertionStatus: " + userAssertionStatus)
$.post(OCC_REC.contextPath + "/occurrences/assertions/add",
{ recordUuid: occUuid,
code: code,
diff --git a/grails-app/controllers/au/org/ala/biocache/hubs/AssertionsController.groovy b/grails-app/controllers/au/org/ala/biocache/hubs/AssertionsController.groovy
index 66c7a0e14..9f3e62e37 100644
--- a/grails-app/controllers/au/org/ala/biocache/hubs/AssertionsController.groovy
+++ b/grails-app/controllers/au/org/ala/biocache/hubs/AssertionsController.groovy
@@ -52,11 +52,22 @@ class AssertionsController {
String comment = params.comment?:''
String userAssertionStatus = params.userAssertionStatus?: ""
String assertionUuid = params.assertionUuid?: ""
+ String relatedRecordId = params.relatedRecordId ?: ''
+ String relatedRecordReason = params.relatedRecordReason ?: ''
UserDetails userDetails = authService?.userDetails() // will return null if not available/not logged in
if (recordUuid && code && userDetails) {
+
+ if (code == '20020' && !relatedRecordId) {
+ render(status: 400, text: 'Duplicate record id not provided')
+ }
+
+ if (code == '20020' && !relatedRecordReason) {
+ render(status: 400, text: 'Duplicate record reason not provided')
+ }
+
log.info("Adding assertion to UUID: ${recordUuid}, code: ${code}, comment: ${comment}, userAssertionStatus: ${userAssertionStatus}, userId: ${userDetails.userId}, userEmail: ${userDetails.email}")
- Map postResponse = webServicesService.addAssertion(recordUuid, code, comment, userDetails.userId, userDetails.displayName, userAssertionStatus, assertionUuid)
+ Map postResponse = webServicesService.addAssertion(recordUuid, code, comment, userDetails.userId, userDetails.displayName, userAssertionStatus, assertionUuid, relatedRecordId, relatedRecordReason)
if (postResponse.statusCode == 201) {
log.info("Called REST service. Assertion should be added" )
diff --git a/grails-app/controllers/au/org/ala/biocache/hubs/OccurrenceController.groovy b/grails-app/controllers/au/org/ala/biocache/hubs/OccurrenceController.groovy
index 6741348a8..efc3b71e1 100644
--- a/grails-app/controllers/au/org/ala/biocache/hubs/OccurrenceController.groovy
+++ b/grails-app/controllers/au/org/ala/biocache/hubs/OccurrenceController.groovy
@@ -30,6 +30,7 @@ import javax.servlet.http.HttpServletRequest
import java.text.SimpleDateFormat
import static au.org.ala.biocache.hubs.TimingUtils.time
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND
/**
* Controller for occurrence searches and records
@@ -662,6 +663,25 @@ class OccurrenceController {
render webServicesService.facetCSVDownload(requestParams), contentType: 'text/csv', fileName: 'data.csv'
}
+ def exists(String id) {
+ // getRecord can return either 1 record or a list of records
+ // if a list returned, asking user to be more specific
+ def record = webServicesService.getRecord(id, false)
+ if (record.occurrences) {
+ render ([error: 'id not unique'] as JSON)
+ } else if (record.keySet()) {
+ def rslt = [:]
+ rslt.scientificName = record?.processed?.classification?.scientificName ?: (record?.raw?.classification?.scientificName ?: '')
+ rslt.stateProvince = record?.processed?.location?.stateProvince ?: (record?.raw?.location?.stateProvince ?: '')
+ rslt.decimalLongitude = record?.processed?.location?.decimalLongitude ?: (record?.raw?.location?.decimalLongitude ?: '')
+ rslt.decimalLatitude = record?.processed?.location?.decimalLatitude ?: (record?.raw?.location?.decimalLatitude ?: '')
+ rslt.eventDate = record?.processed?.event?.eventDate ?: (record?.raw?.event?.eventDate ?: '')
+ render rslt as JSON
+ } else {
+ render status: SC_NOT_FOUND, text: ''
+ }
+ }
+
/**
* JSON webservices for debugging/testing
*/
diff --git a/grails-app/i18n/messages_en.properties b/grails-app/i18n/messages_en.properties
index 8aeb10013..7ae028bd4 100644
--- a/grails-app/i18n/messages_en.properties
+++ b/grails-app/i18n/messages_en.properties
@@ -133,6 +133,8 @@ show.loginorflag.div01.navigator = Click here
show.loginorflag.div02.label = You are logged in as
show.issueform.label01 = Issue type:
show.issueform.label02 = Comment:
+show.issueform.label03 = Duplicate Record ID:
+show.issueform.label04 = Duplicate Reason:
show.issueform.button.submit = Submit
show.issueform.button.cancel = Cancel
show.issueform.button.close = Close
@@ -198,6 +200,8 @@ show.userannotationtemplate.p01.navigator = View more with this annotation
show.userannotationtemplate.p02.navigator = Delete this annotation
show.userannotationtemplate.p03.navigator = Verify this annotation
show.userannotationtemplate.p04.navigator = Delete this verification
+show.userannotationtemplate.relatedrecord.userduplicate.a = View duplicated record
+show.userannotationtemplate.relatedrecord.default.a = View related record
show.headingbar02.title = Record Not Found
show.headingbar02.p01 = The requested record ID
show.headingbar02.p02 = was not found
@@ -825,6 +829,7 @@ unrecognisedInstitutionCode=Institution code not recognised
invalidImageUrl=Image URL invalid
temporalIssue=Temporal issue
userAssertionOther=Other issue
+userDuplicateRecord=Duplicate record
idPreOccurrence=Identification date before occurrence date
georefPostDate=Georeferenced after occurrence date
firstOfMonth=First of the month
@@ -1168,3 +1173,23 @@ dq.warning.dataprofile.buttonright.text = Got it
dq.userpref.defaultprofile = -- Select a profile --
dq.data.profiles.disabled = Data profiles have been disabled for this search
dq.warning.failedtosave = Failed to save user preferences. Please try again
+record.compare_table.heading = You are indicating that
+record.compare_table.source_record.heading = This record
+record.compare_table.target_record.heading = This record ID provided
+related.record.reason.select=-- Select a reason --
+related.record.reason.sameoccurrence=Duplicate occurrence
+related.record.reason.tissuesample=Tissue sample
+related.record.reason.splitspecimen=Split specimen
+related.record.reason.description.sameoccurrence=Is a duplicate occurrence of
+related.record.reason.description.tissuesample=Is a tissue sample of
+related.record.reason.description.splitspecimen=Is a split specimen of
+show.userannotationtemplate.relatedrecord.reason.label=Reason:
+related.record.id.label=record ID
+related.record.name.label=scientific name
+related.record.state.label=state
+related.record.latitude.label=latitude
+related.record.longitude.label=longitude
+related.record.eventdate.label=eventDate
+related.record.reason.explanation.sameoccurrence=This record is a duplicate occurrence of this record:
+related.record.reason.explanation.tissuesample=This record is a tissue sample of this record:
+related.record.reason.explanation.splitspecimen=This record is a split specimen of this record:
diff --git a/grails-app/services/au/org/ala/biocache/hubs/WebServicesService.groovy b/grails-app/services/au/org/ala/biocache/hubs/WebServicesService.groovy
index 4101522c0..3641093b7 100644
--- a/grails-app/services/au/org/ala/biocache/hubs/WebServicesService.groovy
+++ b/grails-app/services/au/org/ala/biocache/hubs/WebServicesService.groovy
@@ -41,7 +41,7 @@ class WebServicesService {
public static final String ENVIRONMENTAL = "Environmental"
public static final String CONTEXTUAL = "Contextual"
- def grailsApplication, facetsCacheServiceBean
+ def grailsApplication, facetsCacheServiceBean, authService
QualityService qualityService
@Value('${dataquality.enabled}')
@@ -194,13 +194,16 @@ class WebServicesService {
* @return Map postResponse
*/
Map addAssertion(String recordUuid, String code, String comment, String userId, String userDisplayName,
- String userAssertionStatus, String assertionUuid) {
+ String userAssertionStatus, String assertionUuid, String relatedRecordId,
+ String relatedRecordReason) {
Map postBody = [
recordUuid: recordUuid,
code: code,
comment: comment,
userAssertionStatus: userAssertionStatus,
assertionUuid: assertionUuid,
+ relatedRecordId: relatedRecordId,
+ relatedRecordReason: relatedRecordReason,
userId: userId,
userDisplayName: userDisplayName,
apiKey: grailsApplication.config.biocache.apiKey
@@ -404,12 +407,15 @@ class WebServicesService {
* @param url
* @return
*/
- JSONElement getJsonElements(String url) {
+ JSONElement getJsonElements(String url, String apiKey = null) {
log.debug "(internal) getJson URL = " + url
def conn = new URL(url).openConnection()
try {
conn.setConnectTimeout(10000)
conn.setReadTimeout(50000)
+ if (apiKey != null) {
+ conn.setRequestProperty('apiKey', apiKey)
+ }
return JSON.parse(conn.getInputStream(), "UTF-8")
} catch (Exception e) {
def error = "Failed to get json from web service (${url}). ${e.getClass()} ${e.getMessage()}, ${e}"
@@ -448,13 +454,17 @@ class WebServicesService {
* @param postParams
* @return postResponse (Map with keys: statusCode (int) and statusMsg (String)
*/
- def Map postFormData(String uri, Map postParams) {
+ def Map postFormData(String uri, Map postParams, String apiKey = null) {
HTTPBuilder http = new HTTPBuilder(uri)
log.debug "POST (form encoded) to ${http.uri}"
Map postResponse = [:]
http.request( Method.POST ) {
+ if (apiKey != null) {
+ headers.'apiKey' = apiKey
+ }
+
send ContentType.URLENC, postParams
response.success = { resp ->
diff --git a/grails-app/views/occurrence/_recordSidebar.gsp b/grails-app/views/occurrence/_recordSidebar.gsp
index 7c4d4b143..03f3aeb9e 100644
--- a/grails-app/views/occurrence/_recordSidebar.gsp
+++ b/grails-app/views/occurrence/_recordSidebar.gsp
@@ -319,19 +319,65 @@
+
+
+
+
" class="btn btn-primary" />
- " class="btn btn-default" onClick="$('#loginOrFlag').modal('hide');"/>
+ " class="btn btn-default" onClick="$('#loginOrFlag').modal('hide');"/>
" class="btn btn-default" style="display:none;"/>
diff --git a/grails-app/views/occurrence/show.gsp b/grails-app/views/occurrence/show.gsp
index 1d636f3a7..8ab393441 100644
--- a/grails-app/views/occurrence/show.gsp
+++ b/grails-app/views/occurrence/show.gsp
@@ -178,6 +178,10 @@
+
+
+
+