diff --git a/build.gradle b/build.gradle index 3eec3896b..56e413bff 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ plugins { id "com.gorylenko.gradle-git-properties" version "2.4.1" } -version "4.5.1" +version "4.6-SNAPSHOT" group "au.org.ala" description "Ecodata" diff --git a/grails-app/controllers/au/org/ala/ecodata/DataSetSummaryController.groovy b/grails-app/controllers/au/org/ala/ecodata/DataSetSummaryController.groovy new file mode 100644 index 000000000..7a5d1a292 --- /dev/null +++ b/grails-app/controllers/au/org/ala/ecodata/DataSetSummaryController.groovy @@ -0,0 +1,64 @@ +package au.org.ala.ecodata + +import org.apache.http.HttpStatus + +class DataSetSummaryController { + + static responseFormats = ['json', 'xml'] + static allowedMethods = [update:['POST', 'PUT'], delete:'DELETE', bulkUpdate: 'POST'] + + ProjectService projectService + + /** Updates a single dataset for a project */ + def update(String projectId) { + Map dataSet = request.JSON + projectId = projectId ?: dataSet.projectId + + if (!projectId) { + render status: HttpStatus.SC_BAD_REQUEST, text: "projectId is required" + return + } + + if (dataSet.projectId && dataSet.projectId != projectId) { + render status: HttpStatus.SC_BAD_REQUEST, text: "projectId must match the data set projectId" + return + } + + respond projectService.updateDataSet(projectId, dataSet) + } + + /** + * Updates multiple data sets for a project. + * This endpoint exists to support the use case of associating multiple data sets with a + * report and updating their publicationStatus when the report is submitted/approved. + * + * This method expects the projectId to be supplied via the URL and the data sets to be supplied in the request + * body as a JSON object with key="dataSets" and value=List of data sets. + */ + def bulkUpdate(String projectId) { + Map postBody = request.JSON + List dataSets = postBody?.dataSets + + if (!projectId) { + render status: HttpStatus.SC_BAD_REQUEST, text: "projectId is required" + return + } + + for (Map dataSet in dataSets) { + if (dataSet.projectId && dataSet.projectId != projectId) { + render status: HttpStatus.SC_BAD_REQUEST, text: "projectId must match the projectId in all supplied data sets" + return + } + } + + respond projectService.updateDataSets(projectId, dataSets) + } + + def delete(String projectId, String dataSetId) { + if (!projectId || !dataSetId) { + render status: HttpStatus.SC_BAD_REQUEST, text: "projectId and dataSetId are required" + return + } + respond projectService.deleteDataSet(projectId, dataSetId) + } +} diff --git a/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy b/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy index 7f85d0b43..c759f7a07 100644 --- a/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy +++ b/grails-app/controllers/au/org/ala/ecodata/UrlMappings.groovy @@ -196,6 +196,12 @@ class UrlMappings { "/ws/project/getDefaultFacets"(controller: "project", action: "getDefaultFacets") "/ws/project/$projectId/dataSet/$dataSetId/records"(controller: "project", action: "fetchDataSetRecords") "/ws/admin/initiateSpeciesRematch"(controller: "admin", action: "initiateSpeciesRematch") + "/ws/dataSetSummary/$projectId/$dataSetId?"(controller :'dataSetSummary') { + + action = [POST:'update', PUT:'update', DELETE:'delete'] + } + + "/ws/dataSetSummary/bulkUpdate/$projectId"(controller:'dataSetSummary', action:'bulkUpdate') "/ws/document/download"(controller:"document", action:"download") diff --git a/grails-app/services/au/org/ala/ecodata/ParatooService.groovy b/grails-app/services/au/org/ala/ecodata/ParatooService.groovy index 7e2ee8013..0e6add911 100644 --- a/grails-app/services/au/org/ala/ecodata/ParatooService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ParatooService.groovy @@ -18,7 +18,7 @@ import static grails.async.Promises.task */ @Slf4j class ParatooService { - static final Object LOCK = new Object() + static final String DATASET_DATABASE_TABLE = 'Database Table' static final int PARATOO_MAX_RETRIES = 3 static final String PARATOO_PROTOCOL_PATH = '/protocols' @@ -186,18 +186,7 @@ class ParatooService { dataSet.orgMintedIdentifier = paratooCollectionId.encodeAsOrgMintedIdentifier() log.info "Minting identifier for Monitor collection: ${paratooCollectionId}: ${dataSet.orgMintedIdentifier}" - Map result - synchronized (LOCK) { - Map latestProject = projectService.get(projectId) - if (!latestProject.custom) { - latestProject.custom = [:] - } - if (!latestProject.custom.dataSets) { - latestProject.custom.dataSets = [] - } - latestProject.custom.dataSets << dataSet - result = projectService.update([custom: latestProject.custom], projectId, false) - } + Map result = projectService.updateDataSet(projectId, dataSet) if (!result.error) { result.orgMintedIdentifier = dataSet.orgMintedIdentifier @@ -232,20 +221,20 @@ class ParatooService { Map authHeader = getAuthHeader() Promise promise = task { + userService.setCurrentUser(userId) asyncFetchCollection(collection, authHeader, userId, project) } promise.onError { Throwable e -> log.error("An error occurred feching ${collection.orgMintedUUID}: ${e.message}", e) + userService.clearCurrentUser() } - def result - synchronized (LOCK) { - Map latestProject = projectService.get(project.id) - Map latestDataSet = latestProject.custom?.dataSets?.find { it.dataSetId == collection.orgMintedUUID } - latestDataSet.putAll(dataSet) - result = projectService.update([custom: latestProject.custom], project.id, false) + promise.onComplete { Map result -> + userService.clearCurrentUser() } + def result = projectService.updateDataSet(project.id, dataSet) + [updateResult: result, promise: promise] } @@ -297,6 +286,11 @@ class ParatooService { surveyDataAndObservations[PARATOO_DATAMODEL_PLOT_LAYOUT] = dataSet.siteId } + dataSet.startDate = config.getStartDate(surveyDataAndObservations) + dataSet.endDate = config.getEndDate(surveyDataAndObservations) + dataSet.format = DATASET_DATABASE_TABLE + dataSet.sizeUnknown = true + // Delete previously created activity so that duplicate species records are not created. // Updating existing activity will also create duplicates since it relies on outputSpeciesId to determine // if a record is new and new ones are created by code. @@ -304,22 +298,12 @@ class ParatooService { activityService.delete(dataSet.activityId, true) } - String activityId = createActivityFromSurveyData(form, surveyDataAndObservations, surveyId, dataSet.siteId, userId) + String activityId = createActivityFromSurveyData(form, surveyDataAndObservations, surveyId, dataSet, userId) List records = recordService.getAllByActivity(activityId) dataSet.areSpeciesRecorded = records?.size() > 0 dataSet.activityId = activityId - dataSet.startDate = config.getStartDate(surveyDataAndObservations) - dataSet.endDate = config.getEndDate(surveyDataAndObservations) - dataSet.format = DATASET_DATABASE_TABLE - dataSet.sizeUnknown = true - - synchronized (LOCK) { - Map latestProject = projectService.get(project.project.projectId) - Map latestDataSet = latestProject.custom?.dataSets?.find { it.dataSetId == collection.orgMintedUUID } - latestDataSet.putAll(dataSet) - projectService.update([custom: latestProject.custom], project.id, false) - } + projectService.updateDataSet(project.id, dataSet) } } } @@ -456,14 +440,19 @@ class ParatooService { * @param siteId * @return */ - private String createActivityFromSurveyData(ActivityForm activityForm, Map surveyObservations, ParatooCollectionId collection, String siteId, String userId) { + private String createActivityFromSurveyData(ActivityForm activityForm, Map surveyObservations, ParatooCollectionId collection, Map dataSet, String userId) { Map activityProps = [ type : activityForm.name, formVersion : activityForm.formVersion, description : "Activity submitted by monitor", projectId : collection.projectId, publicationStatus: "published", - siteId : siteId, + siteId : dataSet.siteId, + startDate : dataSet.startDate, + endDate : dataSet.endDate, + plannedStartDate : dataSet.startDate, + plannedEndDate : dataSet.endDate, + externalIds : [new ExternalId(idType: ExternalId.IdType.MONITOR_MINTED_COLLECTION_ID, externalId: dataSet.dataSetId)], userId : userId, outputs : [[ data: surveyObservations, diff --git a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy index b1ee890db..8fab9f617 100644 --- a/grails-app/services/au/org/ala/ecodata/ProjectService.groovy +++ b/grails-app/services/au/org/ala/ecodata/ProjectService.groovy @@ -26,6 +26,11 @@ class ProjectService { static final ENHANCED = 'enhanced' static final PRIVATE_SITES_REMOVED = 'privatesitesremoved' + /** A Map containing a per-project lock for synchronizing locks for updates. The purpose of this + * is to support concurrent edits on different project data set summaries which are currently modelled as + * an embedded array but can be added and updated by both the UI and the Monitor (Parataoo) application API */ + static final Map PROJECT_UPDATE_LOCKS = Collections.synchronizedMap([:].withDefault{ new Object() }) + GrailsApplication grailsApplication MessageSource messageSource SessionLocaleResolver localeResolver @@ -51,6 +56,8 @@ class ProjectService { grailsApplication.mainContext.commonService }*/ + + def getBrief(listOfIds, version = null) { if (listOfIds) { if (version) { @@ -469,30 +476,37 @@ class ProjectService { } def update(Map props, String id, Boolean shouldUpdateCollectory = true) { - Project project = Project.findByProjectId(id) - if (project) { - // retrieve any project activities associated with the project - List projectActivities = projectActivityService.getAllByProject(id) - props = includeProjectFundings(props) - props = includeProjectActivities(props, projectActivities) + synchronized (PROJECT_UPDATE_LOCKS.get(id)) { + Project project = Project.findByProjectId(id) + if (project) { + // retrieve any project activities associated with the project + List projectActivities = projectActivityService.getAllByProject(id) + props = includeProjectFundings(props) + props = includeProjectActivities(props, projectActivities) - try { - bindEmbeddedProperties(project, props) - commonService.updateProperties(project, props) - if (shouldUpdateCollectory) { - updateCollectoryLinkForProject(project, props) + try { + // Custom currently holds keys "details" and "dataSets". Only update the "custom" properties + // that are supplied in the update, leaving the others intact. + if (project.custom && props.custom) { + project.custom.putAll(props.remove('custom')) + } + bindEmbeddedProperties(project, props) + commonService.updateProperties(project, props) + if (shouldUpdateCollectory) { + updateCollectoryLinkForProject(project, props) + } + return [status: 'ok'] + } catch (Exception e) { + Project.withSession { session -> session.clear() } + def error = "Error updating project ${id} - ${e.message}" + log.error error, e + return [status: 'error', error: error] } - return [status: 'ok'] - } catch (Exception e) { - Project.withSession { session -> session.clear() } - def error = "Error updating project ${id} - ${e.message}" - log.error error, e + } else { + def error = "Error updating project - no such id ${id}" + log.error error return [status: 'error', error: error] } - } else { - def error = "Error updating project - no such id ${id}" - log.error error - return [status: 'error', error: error] } } @@ -1054,4 +1068,75 @@ class ProjectService { records } + /** + * Updates a single data set associated with a project. Because the datasets are stored as an embedded + * array in the Project collection, this method is synchronized on the project to avoid concurrent updates to + * different data sets overwriting each other. + * Due to the way it's been modelled as an embedded array, the client is allowed to supply a dataSetId + * when creating a new data set (e.g. a data set created by a submission from the Monitor app uses the + * submissionId as the dataSetId). + * @param projectId The project to update + * @param dataSet the data set to update. + * @return + */ + Map updateDataSet(String projectId, Map dataSet) { + updateDataSets(projectId, [dataSet]) + } + + /** + * Updates multiple data sets associated with a project at the same time. This method exists to support + * the use case of associating multiple data sets with a report and updating their publicationStatus when + * the report is submitted/approved. + * + * Because the datasets are stored as an embedded + * array in the Project collection, this method is synchronized on the project to avoid concurrent updates to + * different data sets overwriting each other. + * Due to the way it's been modelled as an embedded array, the client is allowed to supply a dataSetId + * when creating a new data set (e.g. a data set created by a submission from the Monitor app uses the + * submissionId as the dataSetId). + * @param projectId The project to update + * @param dataSet the data sets to update. + * @return + */ + Map updateDataSets(String projectId, List dataSets) { + synchronized (PROJECT_UPDATE_LOCKS.get(projectId)) { + Project project = Project.findByProjectId(projectId) + if (!project) { + return [status: 'error', error: "No project exists with projectId=${projectId}"] + } + for (Map dataSet in dataSets) { + if (!dataSet.dataSetId) { + dataSet.dataSetId = Identifiers.getNew(true, '') + } + Map matchingDataSet = project.custom?.dataSets?.find { it.dataSetId == dataSet.dataSetId } + if (matchingDataSet) { + matchingDataSet.putAll(dataSet) + } else { + if (!project.custom) { + project.custom = [:] + } + if (!project.custom?.dataSets) { + project.custom.dataSets = [] + } + project.custom.dataSets.add(dataSet) + } + } + update([custom: project.custom], project.projectId, false) + } + } + + Map deleteDataSet(String projectId, String dataSetId) { + synchronized (PROJECT_UPDATE_LOCKS.get(projectId)) { + Project project = Project.findByProjectId(projectId) + + boolean foundMatchingDataSet = project?.custom?.dataSets?.removeAll { it.dataSetId == dataSetId } + if (!foundMatchingDataSet) { + return [status: 'error', error: 'No such data set'] + } + else { + update([custom: project.custom], project.projectId, false) + } + } + } + } \ No newline at end of file diff --git a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy index e6b0be669..9ccd795c1 100644 --- a/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy +++ b/src/main/groovy/au/org/ala/ecodata/paratoo/ParatooProtocolConfig.groovy @@ -1,7 +1,6 @@ package au.org.ala.ecodata.paratoo import au.org.ala.ecodata.* -import au.org.ala.ecodata.converter.ISODateBindingConverter import au.org.ala.ecodata.metadata.OutputMetadata import au.org.ala.ecodata.metadata.PropertyAccessor import com.fasterxml.jackson.annotation.JsonIgnoreProperties @@ -25,9 +24,6 @@ class ParatooProtocolConfig { String endDatePath = 'end_date_time' String surveyIdPath = 'survey_metadata' String plotVisitPath = 'plot_visit' - String plotProtocolObservationDatePath = "date_time" - String plotVisitStartDatePath = "${plotVisitPath}.start_date" - String plotVisitEndDatePath = "${plotVisitPath}.end_date" String plotLayoutPath = "${plotVisitPath}.plot_layout" String plotLayoutIdPath = "${plotLayoutPath}.id" String plotLayoutPointsPath = "${plotLayoutPath}.plot_points" @@ -55,46 +51,13 @@ class ParatooProtocolConfig { return null } - def date - if (usesPlotLayout) { - List dates = getDatesFromObservation(surveyData) - date = dates ? DateUtil.format(dates.first()) : null - return date + def date = getProperty(surveyData, startDatePath) + if (!date) { + date = getPropertyFromSurvey(surveyData, startDatePath) } - else { - date = getProperty(surveyData, startDatePath) - if (!date) { - date = getPropertyFromSurvey(surveyData, startDatePath) - } - date = getFirst(date) - return removeMilliseconds(date) - } - } - - /** - * Get date from plotProtocolObservationDatePath and sort them. - * @param surveyData - reverse lookup output which includes survey and observation data - * @return - */ - List getDatesFromObservation(Map surveyData) { - Map surveysData = surveyData.findAll { key, value -> - ![ getSurveyAttributeName(), ParatooService.PARATOO_DATAMODEL_PLOT_SELECTION, - ParatooService.PARATOO_DATAMODEL_PLOT_VISIT, ParatooService.PARATOO_DATAMODEL_PLOT_LAYOUT].contains(key) - } - List result = [] - ISODateBindingConverter converter = new ISODateBindingConverter() - surveysData.each { key, value -> - def dates = getProperty(value, plotProtocolObservationDatePath) - dates = dates instanceof List ? dates : [dates] - - result.addAll(dates.collect { String date -> - date ? converter.convert(date, ISODateBindingConverter.FORMAT) : null - }) - } - - result = result.findAll { it != null } - result.sort() + date = getFirst(date) + return removeMilliseconds(date) } def getPropertyFromSurvey(Map surveyData, String path) { @@ -107,21 +70,13 @@ class ParatooProtocolConfig { return null } - def date - if (usesPlotLayout) { - def dates = getDatesFromObservation(surveyData) - date = dates ? DateUtil.format(dates.last()) : null - return date + def date = getProperty(surveyData, endDatePath) + if (!date) { + date = getPropertyFromSurvey(surveyData, endDatePath) } - else { - date = getProperty(surveyData, endDatePath) - if (!date) { - date = getPropertyFromSurvey(surveyData, endDatePath) - } - date = getFirst(date) - return removeMilliseconds(date) - } + date = getFirst(date) + return removeMilliseconds(date) } Map getSurveyId(Map surveyData) { diff --git a/src/main/groovy/au/org/ala/ecodata/reporting/ReportGroups.groovy b/src/main/groovy/au/org/ala/ecodata/reporting/ReportGroups.groovy index bbeeb7be5..d8f3ba5fd 100644 --- a/src/main/groovy/au/org/ala/ecodata/reporting/ReportGroups.groovy +++ b/src/main/groovy/au/org/ala/ecodata/reporting/ReportGroups.groovy @@ -119,6 +119,7 @@ class ReportGroups { static class DateGroup extends SinglePropertyGroupingStrategy { + static final String MISSING_DATE_GROUP_NAME = "Date missing" static DateTimeFormatter parser = ISODateTimeFormat.dateTimeNoMillis().withZone(DateTimeZone.default) DateTimeFormatter dateFormatter List buckets @@ -157,6 +158,10 @@ class ReportGroups { def group(data) { def value = propertyAccessor.getPropertyValue(data) + if (!value) { + return MISSING_DATE_GROUP_NAME // Use a special group for null / empty dates. + } + int result = bucketIndex(value) // we put results with an exact date match into the group where the end date of the bucket matches diff --git a/src/test/groovy/au/org/ala/ecodata/DataSetSummaryControllerSpec.groovy b/src/test/groovy/au/org/ala/ecodata/DataSetSummaryControllerSpec.groovy new file mode 100644 index 000000000..d7dc36f56 --- /dev/null +++ b/src/test/groovy/au/org/ala/ecodata/DataSetSummaryControllerSpec.groovy @@ -0,0 +1,90 @@ +package au.org.ala.ecodata + +import grails.testing.web.controllers.ControllerUnitTest +import org.apache.http.HttpStatus +import spock.lang.Specification + +class DataSetSummaryControllerSpec extends Specification implements ControllerUnitTest { + + ProjectService projectService = Mock(ProjectService) + def setup() { + controller.projectService = projectService + } + + def cleanup() { + } + + void "The update method delegates to the projectService"() { + setup: + String projectId = 'p1' + Map dataSetSummary = [dataSetId:'d1', name:'Data set 1'] + + when: + request.method = 'POST' + request.json = dataSetSummary + controller.update(projectId) + + then: + 1 * projectService.updateDataSet(projectId, dataSetSummary) >> [status:'ok'] + response.json == ['status':'ok'] + + } + + void "A project id must be specified either in the path or as part of the data set summary"() { + setup: + Map dataSetSummary = [dataSetId: 'd1', name: 'Data set 1'] + + when: + request.method = 'POST' + request.json = dataSetSummary + controller.update() + + then: + 0 * projectService.updateDataSet(_, _) + response.status == HttpStatus.SC_BAD_REQUEST + } + + void "The delete method delegates to the projectService"() { + setup: + String projectId = 'p1' + String dataSetSummaryId = 'd1' + + when: + request.method = 'DELETE' + controller.delete(projectId, dataSetSummaryId) + + then: + 1 * projectService.deleteDataSet(projectId, dataSetSummaryId) >> [status:'ok'] + response.json == ['status':'ok'] + } + + void "The bulkUpdate method delegates to the projectService"() { + setup: + String projectId = 'p1' + Map postBody = [dataSets:[[dataSetId:'d1', name:'Data set 1']]] + + when: + request.method = 'POST' + request.json = postBody + controller.bulkUpdate(projectId) + + then: + 1 * projectService.updateDataSets(projectId, postBody.dataSets) >> [status:'ok'] + response.json == ['status':'ok'] + } + + void "If a projectId is present in a dataSet it much match the projectId parameter in bulkUpdate"() { + setup: + String projectId = 'p1' + Map postBody = [dataSets:[[dataSetId:'d1', name:'Data set 1', projectId:'p1'], [dataSetId:'d2', name:'Data set 2', projectId:'p2']]] + + when: + request.method = 'POST' + request.json = postBody + controller.bulkUpdate(projectId) + + then: + 0 * projectService.updateDataSets(_, _) + response.status == HttpStatus.SC_BAD_REQUEST + } +} diff --git a/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy b/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy index f79449bdf..180c1350f 100644 --- a/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/ParatooServiceSpec.groovy @@ -138,19 +138,17 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest> project - 1 * projectService.update(_, projectId, false) >> { data, pId, updateCollectory -> - Map dataSet = data.custom.dataSets[1] // The stubbed project already has a dataSet, so the new one will be index=1 + 1 * projectService.updateDataSet(projectId, _) >> { pId, dataSet -> + pId == projectId assert dataSet.surveyId != null assert dataSet.surveyId.eventTime != null assert dataSet.surveyId.userId == 'org1' @@ -194,10 +192,13 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest> [resp: [collections: ["coarse-woody-debris-survey": [uuid: "1", createdAt: "2023-09-01T00:00:00.123Z", start_date_time: "2023-09-01T00:00:00.123Z", end_date_time: "2023-09-01T00:00:00.123Z"]]]] 1 * tokenService.getAuthToken(true) >> Mock(AccessToken) - 2 * projectService.get(projectId) >> [projectId: projectId, custom: [dataSets: [dataSet]]] - 1 * projectService.update([custom: [dataSets: [expectedDataSetAsync]]], 'p1', false) >> [status: 'ok'] - 1 * projectService.update([custom: [dataSets: [expectedDataSetSync]]], 'p1', false) >> [status: 'ok'] - 1 * activityService.create(_) >> [activityId: '123'] + 1 * projectService.updateDataSet(projectId, expectedDataSetAsync) >> [status: 'ok'] + 1 * projectService.updateDataSet(projectId, expectedDataSetSync) >> [status: 'ok'] + 1 * activityService.create({ + it.startDate == "2023-09-01T00:00:00Z" && it.endDate == "2023-09-01T00:00:00Z" && + it.plannedStartDate == "2023-09-01T00:00:00Z" && it.plannedEndDate == "2023-09-01T00:00:00Z" && + it.externalIds[0].externalId == "d1" && it.externalIds[0].idType == ExternalId.IdType.MONITOR_MINTED_COLLECTION_ID + }) >> [activityId: '123'] 1 * activityService.delete("123", true) >> [status: 'ok'] 1 * recordService.getAllByActivity('123') >> [] 1 * settingService.getSetting('paratoo.surveyData.mapping') >> { @@ -213,6 +214,7 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest> [userId: userId] + 1 * userService.setCurrentUser(userId) and: result.updateResult == [status: 'ok'] @@ -300,14 +302,18 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest> [resp: surveyData] 1 * tokenService.getAuthToken(true) >> Mock(AccessToken) - 2 * projectService.update(_, projectId, false) >> [status: 'ok'] - 2 * projectService.get(projectId) >> [projectId: projectId, custom: [dataSets: [dataSet]]] + 2 * projectService.updateDataSet(projectId, _) >> [status: 'ok'] 1 * siteService.create(_) >> { site = it[0]; [siteId: 's1'] } - 1 * activityService.create(_) >> [activityId: '123'] + 1 * activityService.create({ + it.startDate == "2023-09-22T00:59:47Z" && it.endDate == "2023-09-23T00:59:47Z" && + it.plannedStartDate == "2023-09-22T00:59:47Z" && it.plannedEndDate == "2023-09-23T00:59:47Z" && + it.externalIds[0].externalId == "d1" && it.externalIds[0].idType == ExternalId.IdType.MONITOR_MINTED_COLLECTION_ID + }) >> [activityId: '123'] 1 * recordService.getAllByActivity('123') >> [] 1 * settingService.getSetting('paratoo.surveyData.mapping') >> { (["guid-3": [ @@ -322,6 +328,7 @@ class ParatooServiceSpec extends MongoSpec implements ServiceUnitTest> [userId: userId] + 1 * userService.setCurrentUser(userId) and: site.name == "SATFLB0001 - Control (100 x 100)" diff --git a/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy b/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy index f73b83a3e..a5b01f8e7 100644 --- a/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy +++ b/src/test/groovy/au/org/ala/ecodata/ProjectServiceSpec.groovy @@ -761,4 +761,93 @@ class ProjectServiceSpec extends MongoSpec implements ServiceUnitTest