Skip to content

Commit

Permalink
Merge pull request #3444 from AtlasOfLivingAustralia/feature/issue3414
Browse files Browse the repository at this point in the history
#3414 - summarize species records
  • Loading branch information
chrisala authored Feb 14, 2025
2 parents ddbc2ae + 6a3907f commit 3acaf69
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1162,13 +1162,15 @@ class ProjectController {
}

@PreAuthorise(accessLevel = 'editor')
def getSpeciesRecordsFromActivity (String activityId) {
def getSpeciesRecordsFromActivity (String activityId, String groupBy, String operator) {
if(!activityId) {
render status: HttpStatus.SC_BAD_REQUEST, text: [message: 'Activity ID must be supplied'] as JSON
return
}

render projectService.getSpeciesRecordsFromActivity(activityId) as JSON
groupBy = groupBy ?: ProjectService.DEFAULT_GROUP_BY
operator = operator ?: ProjectService.FLATTEN_BY_SUM
render projectService.getSpeciesRecordsFromActivity(activityId, groupBy, operator) as JSON
}

@PreAuthorise(accessLevel = 'editor')
Expand Down
63 changes: 60 additions & 3 deletions grails-app/services/au/org/ala/merit/ProjectService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class ProjectService {
static final String PLAN_SUBMITTED = 'submitted'
static final String PLAN_UNLOCKED = 'unlocked for correction'
public static final String DOCUMENT_ROLE_APPROVAL = 'approval'
public static final String FLATTEN_BY_SUM = "SUM"
public static final String FLATTEN_BY_COUNT = "COUNT"
public static final String DEFAULT_GROUP_BY = 'scientificName,vernacularName,scientificNameID,individualsOrGroups'

// All projects can use the Plot Selection and Layout, Plot Description and Opportune modules, but
// we don't want users recording data sets for Plot Selection and Layout so it's not included here.
Expand Down Expand Up @@ -2234,12 +2237,12 @@ class ProjectService {
result
}

List getSpeciesRecordsFromActivity (String activityId) {
List getSpeciesRecordsFromActivity (String activityId, String groupBy = DEFAULT_GROUP_BY, String operator = FLATTEN_BY_SUM) {
if (activityId) {
String displayFormat = 'SCIENTIFICNAME(COMMONNAME)'
String url = "${grailsApplication.config.getProperty('ecodata.baseUrl')}record/listForActivity/${activityId}"
def records = webService.getJson(url)?.records

List records = webService.getJson(url)?.records
records = groupRecords(records, groupBy, operator)
records?.each { record ->
record.species = [
scientificName: record.scientificName,
Expand All @@ -2254,4 +2257,58 @@ class ProjectService {
records
}
}

/**
* Groups records by the user defined attributes and applies the operator to the numeric values.
* @param records - dwc records
* @param groupBy - scientificName, individualsOrGroups, etc.
* @param operator - SUM
* @return
*/
List groupRecords (List records, String groupBy = DEFAULT_GROUP_BY, String operator = FLATTEN_BY_SUM) {
if (records && groupBy) {
List groupByAttributes = groupBy.tokenize(',')
// Group the records by the user defined attributes such as scientificName, individualsOrGroups, etc.
Map recordsByGroup = records.groupBy { dwcRecord ->
groupByAttributes.collect { dwcRecord[it] }
}

// For each group, summarize the records by applying the operator
Map groupsAndTheirSummary= recordsByGroup.collectEntries { groupKey, groupedRecords ->
// iterate over the records in the group and summarize them
Map summaryOfGroupedRecords = groupedRecords.inject([:], { newRecord, recordInGroup ->
// iterate over the attributes of the record and apply the operator
recordInGroup.each { dwcAttribute, dwcValue ->
// sum or count all numeric values that are not used for grouping
if (dwcAttribute !in groupByAttributes && dwcValue instanceof Number) {
switch (operator) {
case FLATTEN_BY_COUNT:
// implement count operator
break

case FLATTEN_BY_SUM:
newRecord[dwcAttribute] = (newRecord[dwcAttribute] ?: 0) + dwcValue
break
default:
log.error "Unsupported operator: ${operator}"
}
}
else {
// if not a numeric value, just copy the value to the new record
newRecord[dwcAttribute] = dwcValue
}
}

newRecord
})

// flattens groupedRecords (list) to a map
[(groupKey): summaryOfGroupedRecords]
}

return groupsAndTheirSummary.values().toList()
}

records
}
}
4 changes: 2 additions & 2 deletions src/test/groovy/au/org/ala/merit/ProjectControllerSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -949,10 +949,10 @@ class ProjectControllerSpec extends Specification implements ControllerUnitTest<
def "Get species record for an activity id" (String activityId, int statusCode, int numberOfCalls, def data) {
when:
request.method = 'GET'
controller.getSpeciesRecordsFromActivity (activityId)
controller.getSpeciesRecordsFromActivity (activityId, null, null)
then:
numberOfCalls * projectService.getSpeciesRecordsFromActivity (activityId) >> data
numberOfCalls * projectService.getSpeciesRecordsFromActivity (activityId, _, _) >> data
controller.response.status == statusCode
where:
Expand Down
17 changes: 11 additions & 6 deletions src/test/groovy/au/org/ala/merit/ProjectServiceSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import org.grails.web.converters.marshaller.json.MapMarshaller
import org.joda.time.Period
import spock.lang.Specification
import spock.lang.Unroll

import java.util.concurrent.locks.Lock

/**
* Tests the ProjectService class.
*/
Expand Down Expand Up @@ -1507,13 +1504,21 @@ class ProjectServiceSpec extends Specification implements ServiceUnitTest<Projec
def "Get species records for an activity id and construct species object" (){
setup:
String activityId = 'a1'
def record = [scientificName: "sc1", vernacularName: "vn1", guid: "g1", outputSpeciesId: "o1"]
def record = [scientificName: "sc1", vernacularName: "vn1", guid: "g1", outputSpeciesId: "o1", individualCount: 1, seedMass: 5]
when:
def result = service.getSpeciesRecordsFromActivity(activityId)

then:
1 * webService.getJson( {it.contains("record/listForActivity/"+activityId)}) >> [records:[record], statusCode: HttpStatus.SC_OK]
result == [record + [species: [scientificName: "sc1", commonName: "vn1", outputSpeciesId: "o1", guid: "g1", name: "sc1 (vn1)"]]]
1 * webService.getJson( {it.contains("record/listForActivity/"+activityId)}) >> [records:[record, record], statusCode: HttpStatus.SC_OK]
result == [[scientificName: "sc1", vernacularName: "vn1", guid: "g1", outputSpeciesId: "o1", individualCount: 2, seedMass: 10] + [species: [scientificName: "sc1", commonName: "vn1", outputSpeciesId: "o1", guid: "g1", name: "sc1 (vn1)"]]]

when:
result = service.getSpeciesRecordsFromActivity(activityId, null, null)

then:
1 * webService.getJson( {it.contains("record/listForActivity/"+activityId)}) >> [records:[record, record], statusCode: HttpStatus.SC_OK]
result.size() == 2
result[0] == record + [species: [scientificName: "sc1", commonName: "vn1", outputSpeciesId: "o1", guid: "g1", name: "sc1 (vn1)"]]

}

Expand Down

0 comments on commit 3acaf69

Please sign in to comment.