diff --git a/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationResultsStore.kt b/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationResultsStore.kt index 17370f29625..8187aa1b8e0 100644 --- a/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationResultsStore.kt +++ b/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationResultsStore.kt @@ -8,7 +8,6 @@ import com.terraformation.backend.db.default_schema.SpeciesId import com.terraformation.backend.db.default_schema.tables.references.USERS import com.terraformation.backend.db.forMultiset import com.terraformation.backend.db.tracking.ObservationId -import com.terraformation.backend.db.tracking.ObservationPlotStatus import com.terraformation.backend.db.tracking.PlantingSiteId import com.terraformation.backend.db.tracking.RecordedSpeciesCertainty import com.terraformation.backend.db.tracking.tables.references.MONITORING_PLOTS @@ -271,6 +270,7 @@ class ObservationResultsStore(private val dslContext: DSLContext) { OBSERVATION_PLOTS.COMPLETED_TIME, OBSERVATION_PLOTS.IS_PERMANENT, OBSERVATION_PLOTS.NOTES, + OBSERVATION_PLOTS.STATUS_ID, monitoringPlotsBoundaryField, MONITORING_PLOTS.ID, MONITORING_PLOTS.FULL_NAME, @@ -309,12 +309,7 @@ class ObservationResultsStore(private val dslContext: DSLContext) { val plantingDensity = (totalLive * SQUARE_METERS_PER_HECTARE / areaSquareMeters).roundToInt() - val status = - when { - completedTime != null -> ObservationPlotStatus.Completed - claimedBy != null -> ObservationPlotStatus.Claimed - else -> ObservationPlotStatus.Unclaimed - } + val status = record[OBSERVATION_PLOTS.STATUS_ID]!! ObservationMonitoringPlotResultsModel( boundary = record[monitoringPlotsBoundaryField] as Polygon, diff --git a/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationStore.kt b/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationStore.kt index 8046dee3051..2743bb0501a 100644 --- a/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationStore.kt +++ b/src/main/kotlin/com/terraformation/backend/tracking/db/ObservationStore.kt @@ -583,27 +583,33 @@ class ObservationStore( val rowsUpdated = dslContext .update(OBSERVATION_PLOTS) + .set(OBSERVATION_PLOTS.STATUS_ID, ObservationPlotStatus.Claimed) .set(OBSERVATION_PLOTS.CLAIMED_BY, currentUser().userId) .set(OBSERVATION_PLOTS.CLAIMED_TIME, clock.instant()) .where(OBSERVATION_PLOTS.OBSERVATION_ID.eq(observationId)) .and(OBSERVATION_PLOTS.MONITORING_PLOT_ID.eq(monitoringPlotId)) .and( - OBSERVATION_PLOTS.CLAIMED_BY.isNull.or( - OBSERVATION_PLOTS.CLAIMED_BY.eq(currentUser().userId))) + OBSERVATION_PLOTS.STATUS_ID.eq(ObservationPlotStatus.Unclaimed) + .or( + OBSERVATION_PLOTS.STATUS_ID.eq(ObservationPlotStatus.Claimed) + .and(OBSERVATION_PLOTS.CLAIMED_BY.eq(currentUser().userId)))) .execute() if (rowsUpdated == 0) { - val plotAssignment = + val plotStatus = dslContext - .selectOne() + .select(OBSERVATION_PLOTS.STATUS_ID) .from(OBSERVATION_PLOTS) .where(OBSERVATION_PLOTS.OBSERVATION_ID.eq(observationId)) .and(OBSERVATION_PLOTS.MONITORING_PLOT_ID.eq(monitoringPlotId)) - .fetch() - if (plotAssignment.isEmpty()) { + .fetch { it[OBSERVATION_PLOTS.STATUS_ID]!! } + if (plotStatus.isEmpty()) { throw PlotNotInObservationException(observationId, monitoringPlotId) } else { - throw PlotAlreadyClaimedException(monitoringPlotId) + when (plotStatus.first()) { + ObservationPlotStatus.Claimed -> throw PlotAlreadyClaimedException(monitoringPlotId) + else -> throw PlotAlreadyCompletedException(monitoringPlotId) + } } } } @@ -614,27 +620,31 @@ class ObservationStore( val rowsUpdated = dslContext .update(OBSERVATION_PLOTS) + .set(OBSERVATION_PLOTS.STATUS_ID, ObservationPlotStatus.Unclaimed) .setNull(OBSERVATION_PLOTS.CLAIMED_BY) .setNull(OBSERVATION_PLOTS.CLAIMED_TIME) .where(OBSERVATION_PLOTS.OBSERVATION_ID.eq(observationId)) .and(OBSERVATION_PLOTS.MONITORING_PLOT_ID.eq(monitoringPlotId)) + .and(OBSERVATION_PLOTS.STATUS_ID.eq(ObservationPlotStatus.Claimed)) .and(OBSERVATION_PLOTS.CLAIMED_BY.eq(currentUser().userId)) .execute() if (rowsUpdated == 0) { - val plotClaim = + val plotStatus = dslContext - .select(OBSERVATION_PLOTS.CLAIMED_BY) + .select(OBSERVATION_PLOTS.STATUS_ID) .from(OBSERVATION_PLOTS) .where(OBSERVATION_PLOTS.OBSERVATION_ID.eq(observationId)) .and(OBSERVATION_PLOTS.MONITORING_PLOT_ID.eq(monitoringPlotId)) - .fetch(OBSERVATION_PLOTS.CLAIMED_BY) - if (plotClaim.isEmpty()) { + .fetch { it[OBSERVATION_PLOTS.STATUS_ID]!! } + if (plotStatus.isEmpty()) { throw PlotNotInObservationException(observationId, monitoringPlotId) - } else if (plotClaim.first() == null) { - throw PlotNotClaimedException(monitoringPlotId) } else { - throw PlotAlreadyClaimedException(monitoringPlotId) + when (plotStatus.first()) { + ObservationPlotStatus.Unclaimed -> throw PlotNotClaimedException(monitoringPlotId) + ObservationPlotStatus.Claimed -> throw PlotAlreadyClaimedException(monitoringPlotId) + else -> throw PlotAlreadyCompletedException(monitoringPlotId) + } } } } @@ -676,7 +686,8 @@ class ObservationStore( .fetchOneInto(ObservationPlotsRow::class.java) ?: throw PlotNotInObservationException(observationId, monitoringPlotId) - if (observationPlotsRow.completedTime != null) { + if (observationPlotsRow.statusId == ObservationPlotStatus.Completed || + observationPlotsRow.statusId == ObservationPlotStatus.NotObserved) { throw PlotAlreadyCompletedException(monitoringPlotId) } @@ -710,7 +721,9 @@ class ObservationStore( completedBy = currentUser().userId, completedTime = clock.instant(), notes = notes, - observedTime = observedTime)) + observedTime = observedTime, + statusId = ObservationPlotStatus.Completed, + )) val allPlotsCompleted = dslContext diff --git a/src/test/kotlin/com/terraformation/backend/tracking/db/ObservationStoreTest.kt b/src/test/kotlin/com/terraformation/backend/tracking/db/ObservationStoreTest.kt index f0bff5a2d6c..318e4d551c1 100644 --- a/src/test/kotlin/com/terraformation/backend/tracking/db/ObservationStoreTest.kt +++ b/src/test/kotlin/com/terraformation/backend/tracking/db/ObservationStoreTest.kt @@ -11,6 +11,7 @@ import com.terraformation.backend.db.tracking.MonitoringPlotId import com.terraformation.backend.db.tracking.ObservableCondition import com.terraformation.backend.db.tracking.ObservationId import com.terraformation.backend.db.tracking.ObservationPlotPosition +import com.terraformation.backend.db.tracking.ObservationPlotStatus import com.terraformation.backend.db.tracking.ObservationState import com.terraformation.backend.db.tracking.PlantingSiteId import com.terraformation.backend.db.tracking.RecordedPlantStatus.Dead @@ -172,7 +173,11 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { val monitoringPlotId12 = insertMonitoringPlot(boundary = polygon(2), fullName = "Z1-S1-2", name = "2") val claimedTime12 = Instant.ofEpochSecond(12) - insertObservationPlot(ObservationPlotsRow(claimedBy = userId1, claimedTime = claimedTime12)) + insertObservationPlot( + ObservationPlotsRow( + claimedBy = userId1, + claimedTime = claimedTime12, + statusId = ObservationPlotStatus.Claimed)) val plantingSubzoneId2 = insertPlantingSubzone(fullName = "Z1-S2", name = "S2") @@ -189,7 +194,9 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { completedBy = userId1, completedTime = completedTime21, notes = "Some notes", - observedTime = observedTime21)) + observedTime = observedTime21, + statusId = ObservationPlotStatus.Completed, + )) val expected = listOf( @@ -854,13 +861,17 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { val row = observationPlotsDao.findAll().first() + assertEquals(ObservationPlotStatus.Claimed, row.statusId, "Plot status") assertEquals(user.userId, row.claimedBy, "Claimed by") assertEquals(clock.instant, row.claimedTime, "Claimed time") } @Test fun `updates claim time if plot is reclaimed by current claimant`() { - insertObservationPlot(claimedBy = user.userId, claimedTime = Instant.EPOCH) + insertObservationPlot( + claimedBy = user.userId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.Claimed) clock.instant = Instant.ofEpochSecond(2) @@ -868,6 +879,7 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { val plotsRow = observationPlotsDao.findAll().first() + assertEquals(ObservationPlotStatus.Claimed, plotsRow.statusId, "Plot status is unchanged") assertEquals(user.userId, plotsRow.claimedBy, "Should remain claimed by user") assertEquals(clock.instant, plotsRow.claimedTime, "Claim time should be updated") } @@ -876,11 +888,34 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { fun `throws exception if plot is claimed by someone else`() { val otherUserId = insertUser() - insertObservationPlot(claimedBy = otherUserId, claimedTime = Instant.EPOCH) + insertObservationPlot( + claimedBy = otherUserId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.Claimed) assertThrows { store.claimPlot(observationId, plotId) } } + @Test + fun `throws exception if plot observation status is completed`() { + insertObservationPlot( + claimedBy = user.userId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.Completed) + + assertThrows { store.claimPlot(observationId, plotId) } + } + + @Test + fun `throws exception if plot observation status is not observed`() { + insertObservationPlot( + claimedBy = user.userId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.NotObserved) + + assertThrows { store.claimPlot(observationId, plotId) } + } + @Test fun `throws exception if no permission to update observation`() { insertObservationPlot() @@ -911,12 +946,16 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { @Test fun `releases claim on plot`() { - insertObservationPlot(claimedBy = user.userId, claimedTime = Instant.EPOCH) + insertObservationPlot( + claimedBy = user.userId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.Claimed) store.releasePlot(observationId, plotId) val row = observationPlotsDao.findAll().first() + assertEquals(ObservationPlotStatus.Unclaimed, row.statusId, "Plot status") assertNull(row.claimedBy, "Claimed by") assertNull(row.claimedTime, "Claimed time") } @@ -932,11 +971,34 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { fun `throws exception if plot is claimed by someone else`() { val otherUserId = insertUser() - insertObservationPlot(claimedBy = otherUserId, claimedTime = Instant.EPOCH) + insertObservationPlot( + claimedBy = otherUserId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.Claimed) assertThrows { store.releasePlot(observationId, plotId) } } + @Test + fun `throws exception if plot observation status is completed`() { + insertObservationPlot( + claimedBy = user.userId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.Completed) + + assertThrows { store.releasePlot(observationId, plotId) } + } + + @Test + fun `throws exception if plot observation status is not observed`() { + insertObservationPlot( + claimedBy = user.userId, + claimedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.NotObserved) + + assertThrows { store.releasePlot(observationId, plotId) } + } + @Test fun `throws exception if no permission to update observation`() { insertObservationPlot() @@ -1029,7 +1091,9 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { completedBy = user.userId, completedTime = clock.instant, notes = "Notes", - observedTime = observedTime) + observedTime = observedTime, + statusId = ObservationPlotStatus.Completed, + ) } else { row } @@ -1478,7 +1542,9 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser { claimedTime = Instant.EPOCH, completedBy = user.userId, completedTime = Instant.EPOCH, - observedTime = Instant.EPOCH)) + observedTime = Instant.EPOCH, + statusId = ObservationPlotStatus.Completed, + )) assertThrows { store.completePlot(observationId, plotId, emptySet(), null, Instant.EPOCH, emptyList())