Skip to content

Commit

Permalink
SW-6405 Show observations affected by new shapefile
Browse files Browse the repository at this point in the history
When an admin uploads a new shapefile for a planting site that has active
observations, show a list of the observations that have active plots in any of the
planting zones that are being changed.
  • Loading branch information
sgrimm committed Jan 29, 2025
1 parent 3864b54 commit 9e3f2c6
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,21 @@ class AdminPlantingSitesController(
"${zoneEdit.areaHaDifference.toPlainString()}ha"
} ?: "Create zone ${zoneEdit.desiredModel!!.name}"
}

val affectedObservationIds =
observationStore.fetchActiveObservationIds(
plantingSiteId, edit.plantingZoneEdits.mapNotNull { it.existingModel?.id })
val affectedObservationsMessage =
if (affectedObservationIds.isNotEmpty()) {
"Plots may be replaced in observations with these IDs: " +
affectedObservationIds.joinToString(", ")
} else {
null
}

val changes =
listOf(
listOfNotNull(
affectedObservationsMessage,
"Total change in plantable area: ${edit.areaHaDifference.toPlainString()}ha",
) + zoneChanges

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,42 @@ class ObservationStore(
.filter { it != null && currentUser().canManageObservation(it.id) }
}

/**
* Returns the IDs of any active assigned observations of a planting site that include unobserved
* plots in specific planting zones.
*/
fun fetchActiveObservationIds(
plantingSiteId: PlantingSiteId,
plantingZoneIds: Collection<PlantingZoneId>
): List<ObservationId> {
requirePermissions { readPlantingSite(plantingSiteId) }

if (plantingZoneIds.isEmpty()) {
return emptyList()
}

return with(OBSERVATIONS) {
dslContext
.select(ID)
.from(OBSERVATIONS)
.where(PLANTING_SITE_ID.eq(plantingSiteId))
.and(IS_AD_HOC.eq(false))
.and(STATE_ID.`in`(ObservationState.InProgress, ObservationState.Overdue))
.and(
ID.`in`(
DSL.select(OBSERVATION_PLOTS.OBSERVATION_ID)
.from(OBSERVATION_PLOTS)
.where(
OBSERVATION_PLOTS.monitoringPlots.plantingSubzones.PLANTING_ZONE_ID.`in`(
plantingZoneIds))
.and(
OBSERVATION_PLOTS.STATUS_ID.`in`(
ObservationPlotStatus.Claimed, ObservationPlotStatus.Unclaimed))))
.orderBy(ID)
.fetch(ID.asNonNullable())
}
}

fun countPlots(
plantingSiteId: PlantingSiteId,
isAdHoc: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,95 @@ class ObservationStoreTest : DatabaseTest(), RunsAsUser {
every { user.canUpdateObservation(any()) } returns true
}

@Nested
inner class FetchActiveObservationIds {
@Test
fun `returns observations with active plots in requested zones`() {
val plantingZoneId1 = insertPlantingZone()
insertPlantingSubzone()
val zone1PlotId1 = insertMonitoringPlot()
val zone1PlotId2 = insertMonitoringPlot()
val plantingZoneId2 = insertPlantingZone()
insertPlantingSubzone()
val zone2PlotId1 = insertMonitoringPlot()
val plantingZoneId3 = insertPlantingZone()
insertPlantingSubzone()
val zone3PlotId1 = insertMonitoringPlot()

val observationId1 = insertObservation()
insertObservationPlot(monitoringPlotId = zone1PlotId1)
insertObservationPlot(monitoringPlotId = zone1PlotId2)
insertObservationPlot(monitoringPlotId = zone2PlotId1)
val observationId2 = insertObservation()
insertObservationPlot(monitoringPlotId = zone2PlotId1)
val observationId3 = insertObservation()
insertObservationPlot(monitoringPlotId = zone3PlotId1)

assertEquals(
listOf(observationId1),
store.fetchActiveObservationIds(plantingSiteId, listOf(plantingZoneId1)),
"Observation with two plots in zone should be listed once")
assertEquals(
listOf(observationId1, observationId2),
store.fetchActiveObservationIds(plantingSiteId, listOf(plantingZoneId2)),
"Observations with plots in multiple zones should be returned")
assertEquals(
listOf(observationId1, observationId3),
store.fetchActiveObservationIds(plantingSiteId, listOf(plantingZoneId1, plantingZoneId3)),
"Should match observations in all requested zones")
}

@Test
fun `does not return observation if its plots in the requested zones are completed`() {
insertPlantingZone()
insertPlantingSubzone()
val monitoringPlotId1 = insertMonitoringPlot()
val plantingZoneId2 = insertPlantingZone()
insertPlantingSubzone()
val monitoringPlotId2 = insertMonitoringPlot()

val observationIdWithActivePlotsInBothZones = insertObservation()
insertObservationPlot(monitoringPlotId = monitoringPlotId1)
insertObservationPlot(monitoringPlotId = monitoringPlotId2)

// Active plot in zone 1, completed plot in zone 2
insertObservation()
insertObservationPlot(monitoringPlotId = monitoringPlotId1)
insertObservationPlot(monitoringPlotId = monitoringPlotId2, completedBy = user.userId)

// Abandoned observation
insertObservation(completedTime = Instant.EPOCH, state = ObservationState.Abandoned)
insertObservationPlot(
monitoringPlotId = monitoringPlotId2, statusId = ObservationPlotStatus.NotObserved)

assertEquals(
listOf(observationIdWithActivePlotsInBothZones),
store.fetchActiveObservationIds(plantingSiteId, listOf(plantingZoneId2)))
}

@Test
fun `does not return ad-hoc observation`() {
val plantingZoneId = insertPlantingZone()
insertPlantingSubzone()
insertMonitoringPlot(isAdHoc = true)
insertObservation(isAdHoc = true)
insertObservationPlot()

assertEquals(
emptyList<ObservationId>(),
store.fetchActiveObservationIds(plantingSiteId, listOf(plantingZoneId)))
}

@Test
fun `throws exception if no permission to read planting site`() {
every { user.canReadPlantingSite(any()) } returns false

assertThrows<PlantingSiteNotFoundException> {
store.fetchActiveObservationIds(plantingSiteId, emptyList())
}
}
}

@Nested
inner class FetchObservationsByPlantingSite {
@Test
Expand Down

0 comments on commit 9e3f2c6

Please sign in to comment.