Skip to content

Commit

Permalink
Make Org available for scores, support default values for expressions #…
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisala committed Feb 14, 2025
1 parent be7c6a6 commit e564379
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ class OrganisationService implements DataBinder {
def org = Organisation.findByOrganisationId(id)
if (org) {
List toAggregate = Score.findAllByScoreIdInList(scoreIds)
List outputSummary = reportService.organisationSummary(id, toAggregate, approvedOnly, aggregationConfig) ?: []
List outputSummary = reportService.organisationSummary(org, toAggregate, approvedOnly, aggregationConfig) ?: []

return outputSummary
} else {
Expand Down
11 changes: 7 additions & 4 deletions grails-app/services/au/org/ala/ecodata/ReportService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -529,17 +529,20 @@ class ReportService {

/**
* Returns aggregated scores for a specified project.
* @param organisationId the organisation of interest.
* @param organisation the organisation of interest.
* @param aggregationSpec defines the scores to be aggregated and if any grouping needs to occur.
* [{score:{name: , units:, aggregationType}, groupBy: {entity: <one of 'activity', 'output', 'organisation', 'site>, property: String <the entity property to group by>}, ...]
*
* @return the results of the aggregation. The results will be a List of Maps, the structure of each Map is
* described in @see au.org.ala.ecodata.reporting.Aggregation.results()
*
*/
List organisationSummary(String organisationId, List aggregationSpec, boolean approvedActivitiesOnly = false, Map topLevelAggregationConfig = null) {
List organisationSummary(Organisation organisation, List aggregationSpec, boolean approvedActivitiesOnly = false, Map topLevelAggregationConfig = null) {

List activities = activityService.findAllForOrganisationId(organisationId, 'FLAT')
aggregate(activities, aggregationSpec, approvedActivitiesOnly, topLevelAggregationConfig)
List activities = activityService.findAllForOrganisationId(organisation.organisationId, 'FLAT')
// Make the organisation available to the aggregation as one of the Scores
// needs access to it.
List aggregationData = activities.collect{it+[organisation:organisation]}
aggregate(aggregationData, aggregationSpec, approvedActivitiesOnly, topLevelAggregationConfig)
}
}
32 changes: 28 additions & 4 deletions src/main/groovy/au/org/ala/ecodata/metadata/ExpressionUtil.groovy
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package au.org.ala.ecodata.metadata

import groovy.util.logging.Slf4j
import org.springframework.context.expression.MapAccessor
import org.springframework.expression.AccessException
import org.springframework.expression.EvaluationContext
Expand All @@ -9,14 +10,28 @@ import org.springframework.expression.spel.support.StandardEvaluationContext
import org.springframework.lang.Nullable
import org.springframework.util.Assert

@Slf4j
class ExpressionUtil {


/** Evaluates the supplied expression against the data in the supplied Map */
static def evaluate(Expression expression, Map expressionContext) {
static def evaluateWithDefault(Expression expression, Map expressionContext, defaultValue) {
StandardEvaluationContext context = new StandardEvaluationContext(expressionContext)
context.addPropertyAccessor(new NoExceptionMapAccessor())
expression.getValue(context)
context.addPropertyAccessor(new NoExceptionMapAccessor(null))

def result
try {
result = expression.getValue(context)
if (Double.isNaN(result)) {
result = defaultValue
}
}
catch (Exception e) {
log.debug("Error evaluating expression: ${expression.getExpressionString()}", e.getMessage())
result = defaultValue
}
result

}


Expand All @@ -26,16 +41,25 @@ class ExpressionUtil {
*/
static class NoExceptionMapAccessor extends MapAccessor {

TypedValue defaultValue
NoExceptionMapAccessor(defaultValue) {
this.defaultValue = new TypedValue(defaultValue)
}
@Override
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
Assert.state(target instanceof Map, "Target must be of type Map");
Map<?, ?> map = (Map<?, ?>) target
Object value = map.get(name)
if (value == null && !map.containsKey(name)) {
return TypedValue.NULL
return defaultValue
}
return new TypedValue(value)

}

@Override
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
return target instanceof Map
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ class FilteredAggregationConfig extends AggregationConfig {
}

class ExpressionAggregationConfig extends CompositeAggregationConfig {
/** The expression to evaluate */
String expression

/** The value to return if the expression evaluation fails (e.g. missing variables due to no data, divide by 0) */
def defaultValue
}

class GroupingConfig extends Aggregation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ class ExpressionAggregator extends BaseAggregator {

AggregatorFactory factory = new AggregatorFactory()
Expression expression
def defaultValue = 0

ExpressionAggregator(ExpressionAggregationConfig config) {

ExpressionParser expressionParser = new SpelExpressionParser()
expression = expressionParser.parseExpression(config.expression)
defaultValue = config.defaultValue ?: 0

aggregators = config.childAggregations.collect {
factory.createAggregator(it)
Expand Down Expand Up @@ -60,7 +62,7 @@ class ExpressionAggregator extends BaseAggregator {
}
}

result.result = ExpressionUtil.evaluate(expression, expressionContext)
result.result = ExpressionUtil.evaluateWithDefault(expression, expressionContext, defaultValue)

result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,40 @@ class ExpressionAggregatorSpec extends Specification {
expect:
result.result == 15
}

def "The expression aggregator can accept a default value for when the expression can't be evaluated"() {
Map config = [
label: "score",
expression: "value1/value2",
"defaultValue":0,
childAggregations: [
[
label:"value1",
property:"value1",
type:"SUM"
],
[
label:"value2",
property:"value2",
type:"SUM"
]
]
]

ExpressionAggregator aggregator = new AggregatorFactory().createAggregator(config)

aggregator.aggregate([data:[:]])
AggregationResult result = aggregator.result()

expect:
result.result == 0

when:
aggregator.aggegate([value1:0, value2:0]) // cause a divide by zero
result = aggregator.result()

then:
result.result == 0

}
}

0 comments on commit e564379

Please sign in to comment.