Skip to content

Commit

Permalink
DigiVol Release 6.1.11
Browse files Browse the repository at this point in the history
 - DG-87, DG-142 (#471) Fixed issue where image dimensions were being misred
 - DG-96 Fixed error with Elastic Search throwing errors when attempting to query more than 10000 user transcriptions
 - DG-82 Updated user notebook to display map pins for user transcriptions with analog geo coordinates
 - DG-144 Fixed broken link for Admin user list
 - DG-131 Fixed search bar in main top menu bar
 - DG-66, DG-67 (#530) Fixed issue with 'See Similar Expeditions' button on finished expedition displaying no results
 - DG-90 (#518) Fixed broken images in Wildlife Spotter expeditions
 - DG-70 (#577) Updated expedition list summary cards to display short description instead of long description truncated
 - DG-94 Added a character counter to short description field in expedition settings
 - DG-146 Removed view button/link from user's notebook
  • Loading branch information
cdausmus committed Nov 29, 2024
2 parents f77bc56 + cb94e99 commit 3774ca0
Show file tree
Hide file tree
Showing 20 changed files with 282 additions and 73 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ branches:
only:
- master
- develop
- qa
- /^feature\/.*$/
- /^hotfix\/.*$/
services:
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ plugins {
id "com.github.erdi.webdriver-binaries" version "3.2"
}

version "6.1.10"
version "6.1.11"
group "au.org.ala"
description "Digivol application"

Expand Down Expand Up @@ -161,7 +161,7 @@ dependencies {
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.3' // old: 2.8.0
implementation 'org.apache.commons:commons-compress:1.11'
implementation 'org.apache.commons:commons-pool2:2.11.1' // old: 2.4.2
implementation group: 'commons-io', name: 'commons-io', version: '2.11.0' //''2.5'
implementation group: 'commons-io', name: 'commons-io', version: '2.17.0' //''2.5'
implementation 'org.elasticsearch:elasticsearch:2.4.6'
// optional for elastic search, version should match elastic search optional dep
implementation group: 'org.apache.lucene', name: 'lucene-expressions', version: '4.9.1' //6.0.0 for ES 5.0, 8.11.1 for ES 7.17
Expand Down
10 changes: 8 additions & 2 deletions grails-app/assets/javascripts/digivol-notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@ var notebook = {
* @param id
*/
function load_content(marker, id) {
const taskViewUrl = $('#map').attr('taskview-url');
$.ajax($('#map').attr('infowindow-url') + "/" + id).done(function(data) {
var content =
"<div style='font-size:12px;line-height:1.3em;'>Catalogue No.: " + data.cat + "<br/>Taxon: " + data.name + "<br/>Transcribed by: " + data.transcriber +
"</div>";
"<div style='font-size:12px;line-height:1.3em;'>" +
"Task: " + id +
"<br />File: ";
if (taskViewUrl !== "") content += "<a href=\"" + taskViewUrl + "/" + id + "\" target=\"_blank\">" + data.filename + "</a>";
else content += data.filename;
if (data.name !== "" && data.name !== undefined && data.name !== null) content += "<br />Taxon: " + data.name
content += "</div>";
notebook.infowindow.close();
notebook.infowindow.setContent(content);
notebook.infowindow.open(notebook.map, marker);
Expand Down
14 changes: 12 additions & 2 deletions grails-app/controllers/au/org/ala/volunteer/ImageController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package au.org.ala.volunteer

import grails.converters.JSON
import org.apache.catalina.connector.ClientAbortException
import org.apache.commons.io.FilenameUtils

import javax.imageio.ImageIO

Expand All @@ -27,8 +28,17 @@ class ImageController {

format = format.toLowerCase()
if (!FORMATS.contains(format)) {
render([error: "${format} not supported"] as JSON, status: SC_BAD_REQUEST)
return
// Did the extension get screwed up? Check if the file does exist:
File fileCheck = findImage(encodedPrefix, encodedName)
if (fileCheck) {
// Image is there with a different extension.
log.debug("Found file under different file extension (or broken input): ${fileCheck.name}")
format = FilenameUtils.getExtension(fileCheck.name)?.toLowerCase()
} else {
log.debug("No file found with supported extension: ${encodedName}.${format}")
render([error: "${format} not supported"] as JSON, status: SC_BAD_REQUEST)
return
}
}

if (width < 0 || width > MAX_WIDTH || height < 0 || height > MAX_HEIGHT) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,14 @@ class ProjectController {
return false
}
}

if (params.shortDescription) {
if (params.shortDescription.toString().length() > 500) {
project.errors.rejectValue("shortDescription", "project.shortdescription.toolarge",
"Short Description is too long.")
return false
}
}
}

bindData(project, params)
Expand Down
101 changes: 76 additions & 25 deletions grails-app/controllers/au/org/ala/volunteer/UserController.groovy
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package au.org.ala.volunteer

import com.google.common.base.Stopwatch
import com.google.common.base.Strings
import grails.converters.JSON
import grails.gorm.transactions.Transactional
import org.elasticsearch.action.search.SearchResponse
import org.elasticsearch.action.search.SearchType
import org.springframework.dao.DataIntegrityViolationException

import java.util.regex.Pattern

import static org.springframework.http.HttpStatus.NO_CONTENT

class UserController {
Expand Down Expand Up @@ -387,7 +390,8 @@ class UserController {
if (!user) {
flash.message = message(code: 'default.not.found.message',
args: [message(code: 'user.label', default: 'User'), params.id]) as String
redirect(action: "list")
redirect(action: "adminList")
return
}

def roles = UserRole.findAllByUser(user)
Expand Down Expand Up @@ -470,7 +474,7 @@ class UserController {
else {
flash.message = message(code: 'default.not.found.message',
args: [message(code: 'user.label', default: 'User'), params.id]) as String
redirect(action: "list")
redirect(action: "adminList")
}
}

Expand All @@ -488,7 +492,7 @@ class UserController {
user.delete(flush: true)
flash.message = message(code: 'default.deleted.message',
args: [message(code: 'user.label', default: 'User'), params.id]) as String
redirect(action: "list")
redirect(action: "adminList")
} catch (DataIntegrityViolationException e) {
String message = message(code: 'default.not.deleted.message',
args: [message(code: 'user.label', default: 'User'), params.id]) as String
Expand All @@ -499,7 +503,7 @@ class UserController {
} else {
flash.message = message(code: 'default.not.found.message',
args: [message(code: 'user.label', default: 'User'), params.id]) as String
redirect(action: "list")
redirect(action: "adminList")
}
}

Expand Down Expand Up @@ -618,40 +622,74 @@ class UserController {
log.debug("ajaxGetPoints| Transcription.countByFullyTranscribedBy(): ${sw.toString()}")
sw.reset().start()

final query = """{
"constant_score": {
"filter": {
"and": [
{ "term": { "transcriptions.fullyTranscribedBy": "${userInstance.userId}" } },
{ "nested" :
{
"path" : "fields",
"filter" : { "term" : { "name": "decimalLongitude"}}
}
},
{ "nested" :
{
"path" : "fields",
"filter" : { "term" : { "name": "decimalLongitude"}}
}
final query = """
{
"query": {
"bool": {
"must": [
{
"term": {
"transcriptions.fullyTranscribedBy": "${userInstance.userId}"
}
},
{
"term": {
"fields.name": "decimalLongitude"
}
},
{
"term": {
"fields.name": "decimalLatitude"
}
}
]
}
]
}
}
}"""

def searchResponse = fullTextIndexService.rawSearch(query, SearchType.QUERY_THEN_FETCH, taskCount.intValue(), fullTextIndexService.rawResponse)
// Elastic Search max value.
final int MAX_SEARCH = 10000
def searchResponse = fullTextIndexService.rawSearch(query, SearchType.QUERY_THEN_FETCH, MAX_SEARCH, fullTextIndexService.rawResponse)
sw.stop()
log.debug("ajaxGetPoints| fullTextIndexService.rawSearch(): ${sw.toString()}")
sw.reset().start()

// Regex for detecting traditional latitude/longitude. We will convert to decimal for Google Maps.
def regex = Pattern.compile(/(((\d+)°)?)(((\d+)')?)(((\d+)")?)([NnSsEeWw])/)

def data = searchResponse.hits.hits.collect { hit ->
def field = hit.source['fields']

def pt = field.findAll { value ->
value['name'] == 'decimalLongitude' || value['name'] == 'decimalLatitude'
}.collectEntries { value ->
def dVal = value['value']
def dVal = value['value'] as String
log.debug("ajaxGetPoints| dVal: ${dVal}")

def matcher = regex.matcher(dVal)
if (matcher.find()) {
log.debug("ajaxGetPoints| Group count: ${matcher.groupCount()}")
for (int i = 0; i <= matcher.groupCount(); i++) {
log.debug("match[${i}]: ${matcher.group(i)}")
}
try {
BigDecimal minutes = (getBigDecimalFromString(matcher.group(6).toString()) / new BigDecimal(60))
BigDecimal seconds = (getBigDecimalFromString(matcher.group(9).toString()) / new BigDecimal(3600))
BigDecimal degrees = getBigDecimalFromString(matcher.group(3).toString())
log.debug("Conversion: degrees: [${degrees}], minutes: [${minutes}], seconds: [${seconds}]")
degrees += (minutes + seconds)

// Check direction and assign negative if necessary
if (matcher.group(10).equalsIgnoreCase("S") ||
matcher.group(10).equalsIgnoreCase("W")) {
degrees = -degrees
}

dVal = degrees.toString()
log.debug("dVal: ${dVal}")
} catch (Exception e) {
log.error("Error attempting to convert degrees, minutes and seconds to a decimal value ${dVal}, skipping.", e)
}
}

if (value['name'] == 'decimalLongitude') {
[lng: dVal]
Expand All @@ -669,6 +707,19 @@ class UserController {
render(data as JSON)
}

/**
* Converts a string value to a BigDecimal, returning 0 if the value isn't parseable.
* @param input the value to convert
* @return a BigDecimal or 0 if null/empty.
*/
def getBigDecimalFromString(String input) {
if (!Strings.isNullOrEmpty(input) && !input.equalsIgnoreCase("null")) {
return new BigDecimal(input.toInteger().intValue())
} else {
return new BigDecimal(0)
}
}

def notebookMainFragment() {
def user = User.get(params.int("id"))
//def simpleTemplateEngine = new SimpleTemplateEngine()
Expand Down
1 change: 1 addition & 0 deletions grails-app/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ status.inactive=Inactive
project.archived.description=Archived expeditions have all their task images removed.
project.template.notcompatible=The selected template is not compatible with the expedition's type.
project.template.notavailable=The template {0} is no longer available.
project.shortdescription.toolarge=You have entered too many characters for the Short Description. It must not exceed 500 characters.
project.institution.required=An institution is required.
image.attribution.prefix=Image
index.heading.line1=Decipher our collections,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,17 +402,20 @@ class ProjectService implements EventPublisher {
// Default, if all else fails
def iconImage = grailsLinkGenerator.resource(file:'/iconLabels.png')
def iconLabel = 'Specimens'
def iconName = 'specimens'

if (project.projectType) {
iconImage = projectTypeService.getIconURL(project.projectType)
iconLabel = project.projectType.label
iconName = project.projectType.name
}

// def volunteer = User.findAll("from User where userId in (select distinct fullyTranscribedBy from Task where project_id = ${project.id})")

def ps = new ProjectSummary(project: project)
ps.iconImage = iconImage
ps.iconLabel = iconLabel
ps.iconName = iconName

ps.taskCount = taskCount.toLong()
ps.transcribedCount = transcribedCount.toLong()
Expand Down Expand Up @@ -540,7 +543,7 @@ class ProjectService implements EventPublisher {
// #392 Add Project Type to the tag search (i.e. clicking on Project Type searches by 'tag')
def tagSearch = []
def labelJoinTable = context.select([PROJECT_LABELS.PROJECT_ID]).from(PROJECT_LABELS.leftJoin(LABEL).on(PROJECT_LABELS.LABEL_ID.eq(LABEL.ID))).where(LABEL.VALUE.in(tag))
def typeJoinTable = context.select([PROJECT.ID]).from(PROJECT.leftJoin(PROJECT_TYPE).on(PROJECT.PROJECT_TYPE_ID.eq(PROJECT_TYPE.ID))).where(PROJECT_TYPE.LABEL.in(tag))
def typeJoinTable = context.select([PROJECT.ID]).from(PROJECT.leftJoin(PROJECT_TYPE).on(PROJECT.PROJECT_TYPE_ID.eq(PROJECT_TYPE.ID))).where(PROJECT_TYPE.NAME.in(tag))

tagSearch.add(PROJECT.ID.in(labelJoinTable))
tagSearch.add(PROJECT.ID.in(typeJoinTable))
Expand Down Expand Up @@ -580,7 +583,7 @@ class ProjectService implements EventPublisher {
sortCondition = jNvl(INSTITUTION.NAME, PROJECT.FEATURED_OWNER)
break
case 'type':
sortCondition = PROJECT_TYPE.LABEL
sortCondition = PROJECT_TYPE.NAME
break
default:
sortCondition = jLower(PROJECT.FEATURED_LABEL)
Expand Down
9 changes: 7 additions & 2 deletions grails-app/services/au/org/ala/volunteer/TaskService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -899,8 +899,12 @@ ORDER BY record_idx, name;
fileMap.dir = dir.absolutePath
def file = new File(dir, filename)
file << conn.inputStream
fileMap.raw = file.name
fileMap.localPath = file.getAbsolutePath()

File processedFile = new File(dir, filename)
boolean result = ImageUtils.reorientImage(file, processedFile)

fileMap.raw = processedFile.name
fileMap.localPath = processedFile.getAbsolutePath()
fileMap.localUrlPrefix = urlPrefix + "${projectId}/${taskId}/${multimediaId}/"
fileMap.contentType = conn.contentType
return fileMap
Expand Down Expand Up @@ -1486,6 +1490,7 @@ ORDER BY record_idx, name;
[ id: row.id,
externalIdentifier: row.external_identifier,
isFullyTranscribed: row.is_fully_transcribed,
fullyTranscribedBy: row.fully_transcribed_by,
//fullyValidatedBy: row.validator_display_name,
projectId: row.project_id,
institutionId: row.institution_id,
Expand Down
6 changes: 3 additions & 3 deletions grails-app/taglib/au/org/ala/volunteer/VolunteerTagLib.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -1059,17 +1059,17 @@ class VolunteerTagLib {
def truncate = { attrs, body ->
final ELLIPSIS = attrs.ellipse ?: ''
def maxLength = attrs.maxlength
final bodyText = body().replaceAll("<[^>]*>", '') // strip out html tags
final bodyText = body().replaceAll("<[^>]*>", '').trim() // strip out html tags and white space

if (maxLength == null || !maxLength.isInteger() || maxLength.toInteger() <= 0) {
throw new Exception("The attribute 'maxlength' must an integer greater than 3. Provided value: $maxLength")
throw new Exception("The attribute 'maxlength' must be an integer greater than 3. Provided value: $maxLength")
} else {
maxLength = maxLength.toInteger()
}
if (maxLength <= ELLIPSIS.size()) {
throw new Exception("The attribute 'maxlength' must be greater than 3. Provided value: $maxLength")
}
if (bodyText.length() > maxLength) {
if ((bodyText.length() + (ELLIPSIS.size() + 1)) > maxLength) {
out << bodyText[0..maxLength - (ELLIPSIS.size() + 1)] + ELLIPSIS
} else {
out << bodyText
Expand Down
Loading

0 comments on commit 3774ca0

Please sign in to comment.