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 @@
+ + + +
<%----%> <%-- OR --%> - From 7b6ba197cfecab952c2f7e075a9a40b85fdaaa95 Mon Sep 17 00:00:00 2001 From: alexhuang091 Date: Tue, 9 Mar 2021 08:50:39 +1100 Subject: [PATCH 35/51] updated version to 3.0.3-SNAPSHOT for next iteration --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 64fcb75c7..e1e327624 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } } -version "3.0.5" +version "3.0.3-SNAPSHOT" group "au.org.ala.plugins.grails" apply plugin:"eclipse" From 482432a701c3ca34bd8d11856b4aecc5bb0e6b1a Mon Sep 17 00:00:00 2001 From: "alex.huang" Date: Tue, 23 Mar 2021 08:50:53 +1100 Subject: [PATCH 36/51] Feature/duplicate record (#429) Add duplicate assertion type to flag an issue --- grails-app/assets/javascripts/show.js | 67 +++---------------- grails-app/i18n/messages_en.properties | 28 ++------ .../views/occurrence/_recordSidebar.gsp | 19 +++--- grails-app/views/occurrence/show.gsp | 14 +--- 4 files changed, 28 insertions(+), 100 deletions(-) diff --git a/grails-app/assets/javascripts/show.js b/grails-app/assets/javascripts/show.js index 83febb17b..b092ebcec 100644 --- a/grails-app/assets/javascripts/show.js +++ b/grails-app/assets/javascripts/show.js @@ -79,8 +79,12 @@ $(document).ready(function() { var relatedRecordReason = $('#relatedRecordReason').val(); if (relatedRecordReason === '') { $(col_reason).text(''); - } else { - $(col_reason).text(jQuery.i18n.prop('related.record.reason.description.' + relatedRecordReason)); + } else if (relatedRecordReason === 'sameoccurence') { + $(col_reason).text(jQuery.i18n.prop('related.record.reason.sameoccurrence.description')); + } else if (relatedRecordReason === 'tissuesample') { + $(col_reason).text(jQuery.i18n.prop('related.record.reason.tissuesample.description')); + } else if (relatedRecordReason === 'splitspecimen') { + $(col_reason).text(jQuery.i18n.prop('related.record.reason.splitspecimen.description')); } setIssueFormButtonState(); }) @@ -193,17 +197,10 @@ $(document).ready(function() { relatedRecordReason: relatedRecordReason, }, function (data) { - // when add assertion succeeds, we update alert settings (only when myannotation is enabled) - if (OCC_REC.myAnnotationEnabled) { - var new_state = $('#notifyChangeCheckbox').prop('checked'); - var actionpath = new_state ? "/occurrences/subscribeMyAnnotation" : "/occurrences/unsubscribeMyAnnotation"; - $.post(OCC_REC.contextPath + actionpath); - } - $('#assertionSubmitProgress').css({'display': 'none'}); $("#submitSuccess").html("Thanks for flagging the problem!"); $("#issueFormSubmit").hide(); - $("input#cancel").hide(); + $("input:reset").hide(); $("input#close").show(); //retrieve all assertions $.get(OCC_REC.contextPath + '/assertions/' + OCC_REC.recordUuid, function (data) { // recordUuid=${record.raw.uuid} @@ -232,18 +229,6 @@ $(document).ready(function() { $(el).html(replaceURLWithHTMLLinks(html)); // convert it }); - $('#assertionButton').click(function (e) { - // if myannotation enabled, to retrieve current settings - if (OCC_REC.myAnnotationEnabled) { - // by default off - $("#notifyChangeCheckbox").prop('checked', false); - var getAlerts = OCC_REC.contextPath + "/occurrences/alerts"; - $.getJSON(getAlerts, function (data) { - var myAnnotationEnabled = data && data.myannotation && data.myannotation.length > 0; - $("#notifyChangeCheckbox").prop('checked', myAnnotationEnabled); - }) - } - }) // bind to form "close" button TODO $("input#close").on("click", function(e) { @@ -439,6 +424,7 @@ 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"); @@ -487,50 +473,17 @@ function refreshUserAnnotations(){ $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) { + 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(); + $clone.find('.related-record-reason-span').text(jQuery.i18n.prop('related.record.reason.'+userAssertion.relatedRecordReason)); } if (userAssertion.userRole != null) { $clone.find('.userRole').text(', ' + userAssertion.userRole); diff --git a/grails-app/i18n/messages_en.properties b/grails-app/i18n/messages_en.properties index fbe97664d..35b4bb9c1 100644 --- a/grails-app/i18n/messages_en.properties +++ b/grails-app/i18n/messages_en.properties @@ -124,8 +124,8 @@ show.verifyrecord.btn.cancel = Cancel show.verifydone.message = Record successfully verified show.verifydone.btn.closeverify = Close show.sidebar01.p = This record was transcribed from the label by an online volunteer. It has not yet been validated by the owner institution -show.sidebar01.volunteer.navigator = DigiVol -show.button.viewdraftbutton.span = See draft in DigiVol +show.sidebar01.volunteer.navigator = Biodiversity Volunteer Portal +show.button.viewdraftbutton.span = See draft in Biodiversity Volunteer Portal show.button.assertionbutton.span = Flag an issue show.loginorflag.title = Flag an issue show.loginorflag.div01.label = Login please: @@ -135,7 +135,6 @@ show.issueform.label01 = Issue type: show.issueform.label02 = Comment: show.issueform.label03 = Duplicate Record ID: show.issueform.label04 = Duplicate Reason: -show.issueform.notifyme = Notify me when records I have annotated are updated show.issueform.button.submit = Submit show.issueform.button.cancel = Cancel show.issueform.button.close = Close @@ -678,9 +677,6 @@ basisOfRecord.PreservedSpecimen=Specimen basisOfRecord.Observation=Observation list.link.t6=Custom charts -duplicate_status.D=Duplicate record -duplicate_status.R=Representative record - # OccurenceTagLib record.noNameSupplied=No name supplied alatag.accepted.name=accepted name @@ -1178,22 +1174,12 @@ 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.source_record.heading = This record you are viewing 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.sameoccurence=Same occurence 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: +related.record.reason.sameoccurrence.description=Is the same occurrence as +related.record.reason.tissuesample.description=Is a tissue sample of +related.record.reason.splitspecimen.description=Is a duplicate specimen of diff --git a/grails-app/views/occurrence/_recordSidebar.gsp b/grails-app/views/occurrence/_recordSidebar.gsp index 2b214639a..43f1e91cf 100644 --- a/grails-app/views/occurrence/_recordSidebar.gsp +++ b/grails-app/views/occurrence/_recordSidebar.gsp @@ -1,4 +1,4 @@ - + @@ -145,6 +145,10 @@ .

+ +
@@ -331,7 +335,7 @@ - + @@ -362,7 +366,7 @@ @@ -371,16 +375,9 @@

- - -

- -

-
-

" 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 2e01975cb..c0abd35f8 100644 --- a/grails-app/views/occurrence/show.gsp +++ b/grails-app/views/occurrence/show.gsp @@ -59,8 +59,7 @@ status="s">'${sds}': '${grailsApplication.config.sensitiveDatasets[sds]}'${s < (sensitiveDatasets.size() - 1) ? ',' : ''} }, - hasGoogleKey: ${grailsApplication.config.google.apikey as Boolean}, - myAnnotationEnabled: ${(grailsApplication.config.getProperty("alerts.myannotation.enabled", Boolean, false))} + hasGoogleKey: ${grailsApplication.config.google.apikey as Boolean} } var BC_CONF = OCC_REC; // For compatibility with common JS components which require BC_CONF @@ -704,14 +703,7 @@

-

- - - - - - - + @@ -844,7 +836,7 @@ -


+


${flash.message}

From 722d9d74c463756fab96009784b0d7a1c12945d2 Mon Sep 17 00:00:00 2001 From: "alex.huang" Date: Tue, 30 Mar 2021 13:08:56 +1100 Subject: [PATCH 37/51] enhanced duplicate record flag (#431) --- grails-app/assets/javascripts/show.js | 48 +++++++++++++++---- grails-app/i18n/messages_en.properties | 20 ++++++-- .../views/occurrence/_recordSidebar.gsp | 6 +-- grails-app/views/occurrence/show.gsp | 9 +++- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/grails-app/assets/javascripts/show.js b/grails-app/assets/javascripts/show.js index b092ebcec..e9a49dfc3 100644 --- a/grails-app/assets/javascripts/show.js +++ b/grails-app/assets/javascripts/show.js @@ -79,12 +79,8 @@ $(document).ready(function() { var relatedRecordReason = $('#relatedRecordReason').val(); if (relatedRecordReason === '') { $(col_reason).text(''); - } else if (relatedRecordReason === 'sameoccurence') { - $(col_reason).text(jQuery.i18n.prop('related.record.reason.sameoccurrence.description')); - } else if (relatedRecordReason === 'tissuesample') { - $(col_reason).text(jQuery.i18n.prop('related.record.reason.tissuesample.description')); - } else if (relatedRecordReason === 'splitspecimen') { - $(col_reason).text(jQuery.i18n.prop('related.record.reason.splitspecimen.description')); + } else { + $(col_reason).text(jQuery.i18n.prop('related.record.reason.description.' + relatedRecordReason)); } setIssueFormButtonState(); }) @@ -200,7 +196,7 @@ $(document).ready(function() { $('#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} @@ -424,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"); @@ -473,17 +468,50 @@ function refreshUserAnnotations(){ $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) { + 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-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); diff --git a/grails-app/i18n/messages_en.properties b/grails-app/i18n/messages_en.properties index 35b4bb9c1..7ae028bd4 100644 --- a/grails-app/i18n/messages_en.properties +++ b/grails-app/i18n/messages_en.properties @@ -1174,12 +1174,22 @@ 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 you are viewing +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.sameoccurence=Same occurence +related.record.reason.sameoccurrence=Duplicate occurrence related.record.reason.tissuesample=Tissue sample related.record.reason.splitspecimen=Split specimen -related.record.reason.sameoccurrence.description=Is the same occurrence as -related.record.reason.tissuesample.description=Is a tissue sample of -related.record.reason.splitspecimen.description=Is a duplicate specimen of +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/views/occurrence/_recordSidebar.gsp b/grails-app/views/occurrence/_recordSidebar.gsp index 43f1e91cf..03f3aeb9e 100644 --- a/grails-app/views/occurrence/_recordSidebar.gsp +++ b/grails-app/views/occurrence/_recordSidebar.gsp @@ -335,7 +335,7 @@
- + @@ -366,7 +366,7 @@ @@ -377,7 +377,7 @@

" 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 c0abd35f8..8ab393441 100644 --- a/grails-app/views/occurrence/show.gsp +++ b/grails-app/views/occurrence/show.gsp @@ -703,7 +703,14 @@

-

+ + + + + + + From 98c8ecc75f739626de762751f90bb089f66b3c05 Mon Sep 17 00:00:00 2001 From: "alex.huang" Date: Wed, 31 Mar 2021 16:30:11 +1100 Subject: [PATCH 38/51] release 3.0.3 (#432) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e1e327624..4b1734ba9 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } } -version "3.0.3-SNAPSHOT" +version "3.0.3" group "au.org.ala.plugins.grails" apply plugin:"eclipse" From 374f7b60bf70a397e0cd9fdfa74790e5a7146b36 Mon Sep 17 00:00:00 2001 From: alexhuang091 Date: Wed, 31 Mar 2021 16:38:34 +1100 Subject: [PATCH 39/51] version to 3.0.4-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4b1734ba9..ac351309e 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } } -version "3.0.3" +version "3.0.4-SNAPSHOT" group "au.org.ala.plugins.grails" apply plugin:"eclipse" From 277b5e4282341f66a224f3e8b62a9614dccdd877 Mon Sep 17 00:00:00 2001 From: Nick dos Remedios Date: Wed, 7 Apr 2021 10:18:21 +1000 Subject: [PATCH 40/51] #434 fix duplicate record facet labels Code formatting --- grails-app/i18n/messages_en.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grails-app/i18n/messages_en.properties b/grails-app/i18n/messages_en.properties index 7ae028bd4..dcba2258f 100644 --- a/grails-app/i18n/messages_en.properties +++ b/grails-app/i18n/messages_en.properties @@ -677,6 +677,9 @@ basisOfRecord.PreservedSpecimen=Specimen basisOfRecord.Observation=Observation list.link.t6=Custom charts +duplicate_status.D=Duplicate record +duplicate_status.R=Representative record + # OccurenceTagLib record.noNameSupplied=No name supplied alatag.accepted.name=accepted name From a1a45748c790728d571a33fe6983ae3a25805d80 Mon Sep 17 00:00:00 2001 From: "alex.huang" Date: Wed, 14 Apr 2021 09:04:25 +1000 Subject: [PATCH 41/51] Feature my annotation (#435) * allow user to subscribe to 'my annotation' alert when flagging an issue --- grails-app/assets/javascripts/show.js | 19 +++++++++++++++++++ grails-app/i18n/messages_en.properties | 1 + .../views/occurrence/_recordSidebar.gsp | 7 +++++++ grails-app/views/occurrence/show.gsp | 3 ++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/grails-app/assets/javascripts/show.js b/grails-app/assets/javascripts/show.js index e9a49dfc3..83febb17b 100644 --- a/grails-app/assets/javascripts/show.js +++ b/grails-app/assets/javascripts/show.js @@ -193,6 +193,13 @@ $(document).ready(function() { relatedRecordReason: relatedRecordReason, }, function (data) { + // when add assertion succeeds, we update alert settings (only when myannotation is enabled) + if (OCC_REC.myAnnotationEnabled) { + var new_state = $('#notifyChangeCheckbox').prop('checked'); + var actionpath = new_state ? "/occurrences/subscribeMyAnnotation" : "/occurrences/unsubscribeMyAnnotation"; + $.post(OCC_REC.contextPath + actionpath); + } + $('#assertionSubmitProgress').css({'display': 'none'}); $("#submitSuccess").html("Thanks for flagging the problem!"); $("#issueFormSubmit").hide(); @@ -225,6 +232,18 @@ $(document).ready(function() { $(el).html(replaceURLWithHTMLLinks(html)); // convert it }); + $('#assertionButton').click(function (e) { + // if myannotation enabled, to retrieve current settings + if (OCC_REC.myAnnotationEnabled) { + // by default off + $("#notifyChangeCheckbox").prop('checked', false); + var getAlerts = OCC_REC.contextPath + "/occurrences/alerts"; + $.getJSON(getAlerts, function (data) { + var myAnnotationEnabled = data && data.myannotation && data.myannotation.length > 0; + $("#notifyChangeCheckbox").prop('checked', myAnnotationEnabled); + }) + } + }) // bind to form "close" button TODO $("input#close").on("click", function(e) { diff --git a/grails-app/i18n/messages_en.properties b/grails-app/i18n/messages_en.properties index dcba2258f..911334967 100644 --- a/grails-app/i18n/messages_en.properties +++ b/grails-app/i18n/messages_en.properties @@ -135,6 +135,7 @@ show.issueform.label01 = Issue type: show.issueform.label02 = Comment: show.issueform.label03 = Duplicate Record ID: show.issueform.label04 = Duplicate Reason: +show.issueform.notifyme = Notify me when records I have annotated are updated show.issueform.button.submit = Submit show.issueform.button.cancel = Cancel show.issueform.button.close = Close diff --git a/grails-app/views/occurrence/_recordSidebar.gsp b/grails-app/views/occurrence/_recordSidebar.gsp index 03f3aeb9e..ce0f012f6 100644 --- a/grails-app/views/occurrence/_recordSidebar.gsp +++ b/grails-app/views/occurrence/_recordSidebar.gsp @@ -375,6 +375,13 @@

+ + +

+ +

+
+

" class="btn btn-primary" /> " class="btn btn-default" onClick="$('#loginOrFlag').modal('hide');"/> diff --git a/grails-app/views/occurrence/show.gsp b/grails-app/views/occurrence/show.gsp index 8ab393441..abc0a55ca 100644 --- a/grails-app/views/occurrence/show.gsp +++ b/grails-app/views/occurrence/show.gsp @@ -59,7 +59,8 @@ status="s">'${sds}': '${grailsApplication.config.sensitiveDatasets[sds]}'${s < (sensitiveDatasets.size() - 1) ? ',' : ''} }, - hasGoogleKey: ${grailsApplication.config.google.apikey as Boolean} + hasGoogleKey: ${grailsApplication.config.google.apikey as Boolean}, + myAnnotationEnabled: ${(grailsApplication.config.getProperty("alerts.myannotation.enabled", Boolean, false))} } var BC_CONF = OCC_REC; // For compatibility with common JS components which require BC_CONF From 2130d2b018d11ce37e0715d2fa31bb80fc627573 Mon Sep 17 00:00:00 2001 From: vjrj Date: Fri, 23 Apr 2021 09:57:25 +0200 Subject: [PATCH 42/51] Fix jquery.i18n load errors --- grails-app/assets/javascripts/biocache-hubs.js | 1 + grails-app/assets/javascripts/leafletPlugins.js | 2 +- grails-app/views/home/index.gsp | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/grails-app/assets/javascripts/biocache-hubs.js b/grails-app/assets/javascripts/biocache-hubs.js index c4a92e01a..02a6b264b 100644 --- a/grails-app/assets/javascripts/biocache-hubs.js +++ b/grails-app/assets/javascripts/biocache-hubs.js @@ -4,6 +4,7 @@ /************************************************************\ * i18n \************************************************************/ +//= require jquery_i18n if (typeof BC_CONF != 'undefined' && BC_CONF.hasOwnProperty('contextPath')) { jQuery.i18n.properties({ name: 'messages', diff --git a/grails-app/assets/javascripts/leafletPlugins.js b/grails-app/assets/javascripts/leafletPlugins.js index b9b654463..384df88a6 100644 --- a/grails-app/assets/javascripts/leafletPlugins.js +++ b/grails-app/assets/javascripts/leafletPlugins.js @@ -1,4 +1,4 @@ - +//= require jquery_i18n //= require leaflet/leaflet-src.js //= require leaflet-fullscreen.js //= require leaflet-plugins/layer/tile/Google.js diff --git a/grails-app/views/home/index.gsp b/grails-app/views/home/index.gsp index 3405e4f8a..382bc01a1 100644 --- a/grails-app/views/home/index.gsp +++ b/grails-app/views/home/index.gsp @@ -28,6 +28,8 @@ + + - -