Skip to content

Commit cca67d5

Browse files
authored
SW-7695 Add generic wrapper for observation updates (#3644)
The API for updating observation data for completed plots will need to do the same sanity checks no matter what specific parts of the observation are being updated; add a method to `ObservationService` to support it.
1 parent 40e1da6 commit cca67d5

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

src/main/kotlin/com/terraformation/backend/tracking/ObservationService.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.terraformation.backend.db.tracking.ObservableCondition
1111
import com.terraformation.backend.db.tracking.ObservationId
1212
import com.terraformation.backend.db.tracking.ObservationMediaType
1313
import com.terraformation.backend.db.tracking.ObservationPlotPosition
14+
import com.terraformation.backend.db.tracking.ObservationPlotStatus
1415
import com.terraformation.backend.db.tracking.ObservationState
1516
import com.terraformation.backend.db.tracking.ObservationType
1617
import com.terraformation.backend.db.tracking.PlantingSiteId
@@ -19,6 +20,7 @@ import com.terraformation.backend.db.tracking.tables.daos.ObservationMediaFilesD
1920
import com.terraformation.backend.db.tracking.tables.pojos.ObservationMediaFilesRow
2021
import com.terraformation.backend.db.tracking.tables.pojos.RecordedPlantsRow
2122
import com.terraformation.backend.db.tracking.tables.references.OBSERVATION_MEDIA_FILES
23+
import com.terraformation.backend.db.tracking.tables.references.OBSERVATION_PLOTS
2224
import com.terraformation.backend.file.FileService
2325
import com.terraformation.backend.file.SizedInputStream
2426
import com.terraformation.backend.file.ThumbnailService
@@ -33,11 +35,13 @@ import com.terraformation.backend.tracking.db.InvalidObservationStartDateExcepti
3335
import com.terraformation.backend.tracking.db.ObservationAlreadyStartedException
3436
import com.terraformation.backend.tracking.db.ObservationHasNoSubzonesException
3537
import com.terraformation.backend.tracking.db.ObservationNotFoundException
38+
import com.terraformation.backend.tracking.db.ObservationPlotNotFoundException
3639
import com.terraformation.backend.tracking.db.ObservationRescheduleStateException
3740
import com.terraformation.backend.tracking.db.ObservationStore
3841
import com.terraformation.backend.tracking.db.PlantingSiteNotDetailedException
3942
import com.terraformation.backend.tracking.db.PlantingSiteStore
4043
import com.terraformation.backend.tracking.db.PlotAlreadyCompletedException
44+
import com.terraformation.backend.tracking.db.PlotNotCompletedException
4145
import com.terraformation.backend.tracking.db.PlotNotFoundException
4246
import com.terraformation.backend.tracking.db.PlotNotInObservationException
4347
import com.terraformation.backend.tracking.db.PlotSizeNotReplaceableException
@@ -664,6 +668,33 @@ class ObservationService(
664668
}
665669
}
666670

671+
/**
672+
* Performs update operations on a completed monitoring plot in an observation. The actual updates
673+
* are done by [func]; this is a wrapper that checks permissions and observation status and runs
674+
* the updates in a transaction with the observation locked to avoid problems with concurrent
675+
* updates.
676+
*/
677+
fun <T> updateCompletedPlot(
678+
observationId: ObservationId,
679+
monitoringPlotId: MonitoringPlotId,
680+
func: () -> T,
681+
): T {
682+
requirePermissions { updateObservation(observationId) }
683+
684+
val plotStatus =
685+
dslContext.fetchValue(
686+
OBSERVATION_PLOTS.STATUS_ID,
687+
OBSERVATION_PLOTS.MONITORING_PLOT_ID.eq(monitoringPlotId)
688+
.and(OBSERVATION_PLOTS.OBSERVATION_ID.eq(observationId)),
689+
) ?: throw ObservationPlotNotFoundException(observationId, monitoringPlotId)
690+
691+
if (plotStatus != ObservationPlotStatus.Completed) {
692+
throw PlotNotCompletedException(monitoringPlotId)
693+
}
694+
695+
return observationStore.withLockedObservation(observationId) { _ -> func() }
696+
}
697+
667698
@EventListener
668699
fun on(event: PlantingSiteDeletionStartedEvent) {
669700
deleteMediaWhere(

src/main/kotlin/com/terraformation/backend/tracking/db/Exceptions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ class PlotAlreadyCompletedException(val monitoringPlotId: MonitoringPlotId) :
105105
class PlotNotClaimedException(val monitoringPlotId: MonitoringPlotId) :
106106
MismatchedStateException("Monitoring plot $monitoringPlotId is not claimed by the current user")
107107

108+
class PlotNotCompletedException(val monitoringPlotId: MonitoringPlotId) :
109+
MismatchedStateException("Monitoring plot $monitoringPlotId observation is not completed")
110+
108111
class PlotNotFoundException(val monitoringPlotId: MonitoringPlotId) :
109112
EntityNotFoundException("Monitoring plot $monitoringPlotId not found")
110113

src/test/kotlin/com/terraformation/backend/tracking/ObservationServiceTest.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,15 @@ import com.terraformation.backend.tracking.db.InvalidObservationStartDateExcepti
6565
import com.terraformation.backend.tracking.db.ObservationAlreadyStartedException
6666
import com.terraformation.backend.tracking.db.ObservationHasNoSubzonesException
6767
import com.terraformation.backend.tracking.db.ObservationNotFoundException
68+
import com.terraformation.backend.tracking.db.ObservationPlotNotFoundException
6869
import com.terraformation.backend.tracking.db.ObservationRescheduleStateException
6970
import com.terraformation.backend.tracking.db.ObservationStore
7071
import com.terraformation.backend.tracking.db.ObservationTestHelper
7172
import com.terraformation.backend.tracking.db.PlantingSiteNotDetailedException
7273
import com.terraformation.backend.tracking.db.PlantingSiteNotFoundException
7374
import com.terraformation.backend.tracking.db.PlantingSiteStore
7475
import com.terraformation.backend.tracking.db.PlotAlreadyCompletedException
76+
import com.terraformation.backend.tracking.db.PlotNotCompletedException
7577
import com.terraformation.backend.tracking.db.PlotNotInObservationException
7678
import com.terraformation.backend.tracking.db.PlotSizeNotReplaceableException
7779
import com.terraformation.backend.tracking.db.ScheduleObservationWithoutPlantsException
@@ -2766,6 +2768,76 @@ class ObservationServiceTest : DatabaseTest(), RunsAsDatabaseUser {
27662768
}
27672769
}
27682770

2771+
@Nested
2772+
inner class UpdateCompletedPlot {
2773+
private lateinit var monitoringPlotId: MonitoringPlotId
2774+
private lateinit var observationId: ObservationId
2775+
2776+
@BeforeEach
2777+
fun setUpPlot() {
2778+
insertPlantingZone()
2779+
insertPlantingSubzone()
2780+
monitoringPlotId = insertMonitoringPlot()
2781+
observationId = insertObservation()
2782+
insertObservationPlot(completedBy = user.userId)
2783+
}
2784+
2785+
@Test
2786+
fun `calls function to perform updates`() {
2787+
val initial = dslContext.fetchSingle(OBSERVATION_PLOTS)
2788+
2789+
service.updateCompletedPlot(observationId, monitoringPlotId) {
2790+
dslContext.update(OBSERVATION_PLOTS).set(OBSERVATION_PLOTS.NOTES, "new notes").execute()
2791+
}
2792+
2793+
val expected = initial.copy().apply { notes = "new notes" }
2794+
2795+
assertTableEquals(expected)
2796+
}
2797+
2798+
@Test
2799+
fun `rolls back changes if function throws exception`() {
2800+
val expected = dslContext.fetch(OBSERVATION_PLOTS)
2801+
2802+
assertThrows<IllegalStateException> {
2803+
service.updateCompletedPlot(observationId, monitoringPlotId) {
2804+
dslContext.update(OBSERVATION_PLOTS).set(OBSERVATION_PLOTS.NOTES, "new notes").execute()
2805+
throw IllegalStateException("oops")
2806+
}
2807+
}
2808+
2809+
assertTableEquals(expected)
2810+
}
2811+
2812+
@Test
2813+
fun `throws exception if plot is not in specified observation`() {
2814+
val otherObservationId = insertObservation()
2815+
2816+
assertThrows<ObservationPlotNotFoundException> {
2817+
service.updateCompletedPlot(otherObservationId, monitoringPlotId) {}
2818+
}
2819+
}
2820+
2821+
@Test
2822+
fun `throws exception if plot is not completed yet`() {
2823+
val otherPlotId = insertMonitoringPlot()
2824+
insertObservationPlot()
2825+
2826+
assertThrows<PlotNotCompletedException> {
2827+
service.updateCompletedPlot(observationId, otherPlotId) {}
2828+
}
2829+
}
2830+
2831+
@Test
2832+
fun `throws exception if no permission to update observation`() {
2833+
deleteOrganizationUser()
2834+
2835+
assertThrows<ObservationNotFoundException> {
2836+
service.updateCompletedPlot(observationId, monitoringPlotId) {}
2837+
}
2838+
}
2839+
}
2840+
27692841
/**
27702842
* Inserts a permanent monitoring plot with a given index. By default, the plots are stacked
27712843
* northward, that is, index 1 is at y=0, index 2 is at y=1, and index 3 is at y=2.

0 commit comments

Comments
 (0)