Skip to content

Commit 2cb280c

Browse files
committed
Allow updates to projects via project load mechanism #3466
1 parent 2ece5c6 commit 2cb280c

File tree

5 files changed

+78
-52
lines changed

5 files changed

+78
-52
lines changed

grails-app/services/au/org/ala/merit/ImportService.groovy

+2-6
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,7 @@ class ImportService {
307307
mu?.managementUnitId
308308
}
309309
refreshOrganisationList()
310-
def mapper = new GmsMapper(metadataService.activitiesModel(), metadataService.programsModel(), organisations, abnLookupService, metadataService.getOutputTargetScores(), programs, managementUnits)
311-
310+
def mapper = new GmsMapper(metadataService.activitiesModel(), metadataService.programsModel(), organisations, abnLookupService, metadataService.getOutputTargetScores(), programs, managementUnits, false, update)
312311
def action = preview?{rows -> mapProjectRows(rows, status, mapper, update)}:{rows -> importAll(rows, status, mapper, update)}
313312

314313
Map result = [:]
@@ -360,7 +359,7 @@ class ImportService {
360359

361360
def mapProjectRows(projectRows, List status, GmsMapper mapper, Boolean update) {
362361

363-
Map mappingResults = mapper.mapProject(projectRows)
362+
Map mappingResults = mapper.mapProject(projectRows, update)
364363

365364
String grantId = mappingResults.project.grantId
366365
String externalId = mappingResults.project.externalId
@@ -394,9 +393,6 @@ class ImportService {
394393
def editorEmail = projectDetails.project.remove('editorEmail')
395394
def editorEmail2 = projectDetails.project.remove('editorEmail2')
396395

397-
//When projects are loaded into MERIT via CSV upload, they are given a status of "Application".
398-
projectDetails.project.status ?: 'application'
399-
400396
// Create the organisation first so we can link it to the project.
401397
if (projectDetails.organisation) {
402398
Map orgCreationResult = organisationService.update(null, projectDetails.organisation)

grails-app/views/admin/import.gsp

+1-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@
3434
</p>
3535
<div data-bind="if:update">
3636
<p class="alert alert-warning" >
37-
Please note this function will replace all project information for each project. <br/>
38-
It is designed for use with grants hub data and to fix newly loaded projects. Please test changes
39-
to existing MERIT projects in the staging system before running the update in production.
37+
Please test changes to existing MERIT projects in the staging system before running the update in production.
4038
</p>
4139
</div>
4240
<div class="form-check">

src/main/groovy/au/org/ala/merit/GmsMapper.groovy

+71-39
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package au.org.ala.merit
22

33
import au.com.bytecode.opencsv.CSVWriter
44
import org.apache.commons.lang.WordUtils
5-
import org.apache.commons.validator.EmailValidator
5+
import org.apache.commons.validator.routines.EmailValidator
66

77
import java.text.DecimalFormat
88
import java.text.SimpleDateFormat
@@ -95,6 +95,7 @@ class GmsMapper {
9595

9696
def geographicInfoMapping = [
9797
NATIONWIDE:[name:'nationwide', type:'boolean', description:'If true, this project does not have a primary state'],
98+
STATEWIDE:[name:'statewide', type:'boolean', description:'If true, this project does not have a primary electorate'],
9899
PRIMARY_STATE:[name:'primaryState', type:'string', description:'The primary state to be manually assigned to this project'],
99100
PRIMARY_ELECTORATE:[name:'primaryElectorate', type:'string', description:'The primary electorate to be manually assigned to this project'],
100101
OTHER_ELECTORATES:[name:'otherElectorates', type:'list', description:'Other electorates to be manually assigned to this project. Enter as a comma separated list'],
@@ -135,6 +136,8 @@ class GmsMapper {
135136
]
136137

137138
private boolean includeProgress
139+
/** If the mapping is being done to update a project, mandatory fields aren't required as they won't be updated if absent */
140+
private boolean mapForUpdate = false
138141

139142
public GmsMapper() {
140143
this.activitiesModel = []
@@ -144,9 +147,10 @@ class GmsMapper {
144147
this.programs = [:]
145148
this.managementUnits = [:]
146149
includeProgress = false
150+
mapForUpdate = false
147151
}
148152

149-
GmsMapper(activitiesModel, programModel, organisations, abnLookup, List<Map> scores, Map programs = [:], Map managementUnits = [:], includeProgress = false) {
153+
GmsMapper(activitiesModel, programModel, organisations, abnLookup, List<Map> scores, Map programs = [:], Map managementUnits = [:], includeProgress = false, mapForUpdate = false) {
150154
this.activitiesModel = activitiesModel
151155
this.programModel = programModel
152156
this.includeProgress = includeProgress
@@ -155,6 +159,7 @@ class GmsMapper {
155159
this.programs = programs
156160
this.managementUnits = managementUnits
157161
this.abnLookupService = abnLookup
162+
this.mapForUpdate = mapForUpdate
158163
}
159164

160165
/** Creates a CSV file in the format required to import projects into MERIT with additional instructions */
@@ -222,29 +227,21 @@ class GmsMapper {
222227
return errors
223228
}
224229

225-
def mapProject(projectRows) {
230+
def mapProject(projectRows, boolean update = false) {
226231

227232
List errors = []
228233
List messages = []
229-
Map result = gmsToMerit(projectRows[0], projectMapping) // All project rows have the project details.
234+
Map result = csvToProjectProperties(projectRows[0], projectMapping, update) // All project rows have the project details.
230235
def project = result.mappedData
231-
project.projectType = 'works'
232-
project.isMERIT = true
236+
if (!update) {
237+
// Set some default project properties for the MERIT hub
238+
project.projectType = 'works'
239+
project.isMERIT = true
240+
}
233241

234-
mapGeographicInfo(projectRows[0], project, errors)
242+
mapGeographicInfo(projectRows[0], project, update, errors)
235243

236-
237-
String programName = project.associatedSubProgram ?: project.associatedProgram
238-
Map program = programs[programName]
239-
String programId = program?.programId
240-
if (programId) {
241-
project.remove('associatedProgram')
242-
project.remove('associatedSubProgram')
243-
project.programId = programId
244-
}
245-
else {
246-
errors << "Program ${programName} doesn't match an existing MERIT programme"
247-
}
244+
mapProgram(project, update, errors)
248245

249246
if (project.managementUnitName) {
250247
String managementUnitName = project.remove('managementUnitName')
@@ -254,10 +251,9 @@ class GmsMapper {
254251
}
255252
}
256253

257-
Map organisation = mapOrganisation(project, program, errors, messages)
254+
Map organisation = mapOrganisation(project, update, errors, messages)
258255

259256
errors.addAll(result.errors)
260-
project.planStatus = project.planStatus ?: 'not approved'
261257

262258
mapRisks(projectRows, project, errors)
263259

@@ -270,18 +266,44 @@ class GmsMapper {
270266
project.custom = [details:meriPlan]
271267
}
272268

269+
if (update) {
270+
Set propertiesToUpdate = new HashSet(project.keySet())
271+
propertiesToUpdate.remove('grantId') // This identifies the project and won't be updated.
272+
messages << "Update: ${propertiesToUpdate}"
273+
}
274+
273275
[project:project, sites:sites, activities:activities, errors:errors, messages:messages, organisation:organisation]
274276

275277
}
276278

279+
private Map mapProgram(LinkedHashMap<Object, Object> project, boolean update, ArrayList errors) {
280+
String programName = project.associatedSubProgram ?: project.associatedProgram
281+
if (!programName) {
282+
if (!update) {
283+
errors << "Please supply a program name (PROGRAM_NM) or sub-program name (ROUND_NM) for the project"
284+
}
285+
return
286+
}
287+
Map program = programs[programName]
288+
String programId = program?.programId
289+
if (programId) {
290+
project.remove('associatedProgram')
291+
project.remove('associatedSubProgram')
292+
project.programId = programId
293+
} else {
294+
errors << "Program ${programName} doesn't match an existing MERIT programme"
295+
}
296+
program
297+
}
298+
277299
private Map findExistingOrganisation(String organisationId, String abn) {
278300
organisations.find{
279301
(abn && (it.abn == abn)) ||
280302
(organisationId && (it.organisationId == organisationId)) }
281303

282304
}
283305

284-
private Map mapOrganisation(Map project, Map program, List errors, List messages) {
306+
private Map mapOrganisation(Map project, boolean update, List errors, List messages) {
285307
Map organisation
286308
Map abnLookup
287309
String error = null
@@ -339,7 +361,7 @@ class GmsMapper {
339361
project.associatedOrgs = [
340362
[organisationId:organisation.organisationId, name: contractName ?: organisation.name, description:description]]
341363
}
342-
} else {
364+
} else if (!update) {
343365
error = "Please supply an organisationId (ORG_ID) or ABN (ABN) for the project"
344366
}
345367
if (error) {
@@ -395,7 +417,7 @@ class GmsMapper {
395417
def count = siteRows.size()
396418
siteRows.eachWithIndex {siteRow, i ->
397419

398-
def siteResult = gmsToMerit(siteRow, siteMapping)
420+
def siteResult = csvToProjectProperties(siteRow, siteMapping)
399421
def site = siteResult.mappedData
400422
errors.addAll(siteResult.errors)
401423

@@ -432,7 +454,7 @@ class GmsMapper {
432454
def activityRows = projectRows.findAll{it[DATA_TYPE_COLUMN] == ACTIVITY_DATA_TYPE && it[DATA_SUB_TYPE_COLUMN] == ACTIVITY_DATA_SUB_TYPE}
433455
def activities = []
434456
activityRows.eachWithIndex { activityRow, i ->
435-
def activityResult = gmsToMerit(activityRow, activityMapping)
457+
def activityResult = csvToProjectProperties(activityRow, activityMapping)
436458
def mappedActivity = activityResult.mappedData
437459
errors.addAll(activityResult.errors)
438460

@@ -526,7 +548,7 @@ class GmsMapper {
526548
def risks = []
527549
riskRows.eachWithIndex { riskRow, i ->
528550

529-
def result = gmsToMerit(riskRow, riskMapping)
551+
def result = csvToProjectProperties(riskRow, riskMapping)
530552
errors.addAll(result.errors)
531553
def risk = [
532554
"threat" : "",
@@ -546,19 +568,19 @@ class GmsMapper {
546568
}
547569
}
548570

549-
private void mapGeographicInfo(Map rowData, Map project, List errors) {
571+
private void mapGeographicInfo(Map rowData, Map project, boolean update, List errors) {
550572

551-
Map result = gmsToMerit(rowData, geographicInfoMapping)
573+
Map result = csvToProjectProperties(rowData, geographicInfoMapping, update)
552574
if (result.mappedData) {
553575
project.geographicInfo = result.mappedData
554576
}
555-
errors.addAll(errors)
577+
errors.addAll(result.errors)
556578
}
557579

558580
private def mapTarget(rowMap) {
559581

560582
def errors = []
561-
def map = gmsToMerit(rowMap, outputTargetColumnMapping)
583+
def map = csvToProjectProperties(rowMap, outputTargetColumnMapping)
562584
def target = map.mappedData
563585
errors.addAll(map.errors)
564586

@@ -594,7 +616,7 @@ class GmsMapper {
594616
[mappedData:result, errors: errors]
595617
}
596618

597-
private def gmsToMerit(Map rowMap, Map mapping) {
619+
private def csvToProjectProperties(Map rowMap, Map mapping, boolean update = false) {
598620
def result = [:]
599621
def errors = []
600622
mapping.each { Map.Entry e ->
@@ -605,16 +627,20 @@ class GmsMapper {
605627
String propertyName = columnMapping.name
606628
if (columnMapping.multipleColumnsSupported) {
607629
List values = findMultiValue(mappingColumnName, rowMap)
608-
if (!result[propertyName]) {
630+
if (!result[propertyName] && !update) {
609631
result[propertyName] = []
610632
}
611633
values = values.findAll{it}
612-
result[propertyName].addAll(values.collect{convertByType(it, columnMapping)}.findAll{it})
634+
if (values || !update) {
635+
result[propertyName].addAll(values.collect{convertByType(it, columnMapping, update)}.findAll{it})
636+
}
613637
}
614638
else {
615-
result[propertyName] = convertByType(rowMap[mappingColumnName], columnMapping)
639+
def value = rowMap[mappingColumnName]?.trim()
640+
if (value || !update) { // If we are doing an update, ignore empty values.
641+
result[propertyName] = convertByType(rowMap[mappingColumnName], columnMapping, update)
642+
}
616643
}
617-
618644
}
619645
catch (Exception ex) {
620646
errors << "Error converting value: ${rowMap[mappingColumnName]} from row ${rowMap.index} column: ${mappingColumnName}, ${ex.getMessage()}"
@@ -627,22 +653,25 @@ class GmsMapper {
627653
private List findMultiValue(String mappingColumnName, Map rowMap) {
628654
rowMap.findAll { String key, def value ->
629655
key.startsWith(mappingColumnName)
630-
}.collect{it.value}
656+
}.collect{it.value?.trim()}
631657
}
632658

633-
private def convertByType(String value, Map mapping) {
659+
private def convertByType(String value, Map mapping, boolean update) {
634660
String type = mapping.type
635661
value = value?value.trim():''
636662
def result = null
637663
switch (type) {
638664
case 'date':
639-
result = convertDate(value, mapping.mandatory)
665+
result = convertDate(value, mapping.mandatory && !update)
640666
break
641667
case 'decimal':
642668
result = convertDecimal(value)
643669
break
644670
case 'string':
645-
result = value ?: (mapping['default']?:value)
671+
result = value
672+
if (!result && !update) {
673+
result = mapping['default']?:value
674+
}
646675
break
647676
case 'url':
648677
URI.create(value) // validation purposes only
@@ -669,6 +698,9 @@ class GmsMapper {
669698
case 'list':
670699
result = value?.split(',').collect{it.trim()}.findAll{it}
671700
break
701+
case 'boolean':
702+
result = value?.toBoolean() // accept "y"/"true"/"1" as true
703+
break
672704
default:
673705
throw new IllegalArgumentException("Unsupported type: ${type}")
674706
}

src/test/groovy/au/org/ala/merit/GmsMapperSpec.groovy

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class GmsMapperSpec extends Specification{
3030
activitiesModel = JSON.parse(new InputStreamReader(getClass().getResourceAsStream('/activities-model.json')))
3131
Map programModel = [programs:[[name:'Green Army']]]
3232
List organisations = [[ organisationId: "123", name:'Test org 1', abn:'12345678901'], [organisationId:'2', name:"Org 2", abn:""]]
33-
gmsMapper = new GmsMapper(activitiesModel, programModel, organisations, abnLookupService, scores, programs)
33+
gmsMapper = new GmsMapper(activitiesModel, programModel, organisations, abnLookupService, scores, programs, [:], false, false)
3434
}
3535

3636
/**

src/test/groovy/au/org/ala/merit/ImportServiceSpec.groovy

+3-3
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class ImportServiceSpec extends Specification implements ServiceUnitTest<ImportS
114114

115115
def "The import service can create projects that have been loaded and mapped via CSV"() {
116116
setup:
117-
GmsMapper mapper = new GmsMapper(activitiesModel, [:], [[organisationId:"123", name:"Test organisation"]],abnLookupService, scores, ["Test program":["programId":"p1"]], ["Test MU":"m1"])
117+
GmsMapper mapper = new GmsMapper(activitiesModel, [:], [[organisationId:"123", name:"Test organisation"]],abnLookupService, scores, ["Test program":["programId":"p1"]], ["Test MU":"m1"], false, false)
118118
List projectRows = projectRowData()
119119
List status = []
120120
String projectId = 'p1'
@@ -230,7 +230,7 @@ class ImportServiceSpec extends Specification implements ServiceUnitTest<ImportS
230230

231231
def "A project won't be imported if update=false and MERIT already has a project with the same grantId/externalId"() {
232232
setup:
233-
GmsMapper mapper = new GmsMapper(activitiesModel, [:], [[organisationId:"123", name:"Test organisation"]],abnLookupService, scores, ["Test program":[programId:"p1"]], ["Test MU":"m1"])
233+
GmsMapper mapper = new GmsMapper(activitiesModel, [:], [[organisationId:"123", name:"Test organisation"]],abnLookupService, scores, ["Test program":[programId:"p1"]], ["Test MU":"m1"], false, false)
234234
List projectRows = projectRowData()
235235
List status = []
236236
String projectId = 'p1'
@@ -251,7 +251,7 @@ class ImportServiceSpec extends Specification implements ServiceUnitTest<ImportS
251251

252252
def "A project won't be updated if update=true and MERIT does not have a project with the same grantId/externalId"() {
253253
setup:
254-
GmsMapper mapper = new GmsMapper(activitiesModel, [:], [[organisationId:"123", name:"Test organisation"]],abnLookupService, scores, ["Test program":[programId:"p1"]], ["Test MU":"m1"])
254+
GmsMapper mapper = new GmsMapper(activitiesModel, [:], [[organisationId:"123", name:"Test organisation"]],abnLookupService, scores, ["Test program":[programId:"p1"]], ["Test MU":"m1"], false, false)
255255
List projectRows = projectRowData()
256256
List status = []
257257
String projectId = 'p1'

0 commit comments

Comments
 (0)