Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3414 - summarize species records #3444

Merged
merged 3 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading