From afda7b34c9985919b3b2c8b8e522fa829db9ba71 Mon Sep 17 00:00:00 2001 From: Steven Grimm <1248649+sgrimm@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:32:29 -0800 Subject: [PATCH] SW-6251 Populate monitoring plot histories table (#2611) When monitoring plots are created or planting site maps are edited, record their details in the new `monitoring_plot_histories` table. --- .../backend/tracking/db/PlantingSiteStore.kt | 60 +++++++++++++++- .../PlantingSiteStoreApplyEditTest.kt | 70 +++++++++++++------ .../PlantingSiteStoreCreateSiteTest.kt | 3 + .../PlantingSiteStoreCreateTemporaryTest.kt | 32 ++++++--- .../PlantingSiteStoreEnsurePermanentTest.kt | 54 ++++++++++++-- 5 files changed, 183 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt b/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt index 8f289d8e74bd..5a28ae9707a6 100644 --- a/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt +++ b/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt @@ -12,6 +12,7 @@ import com.terraformation.backend.db.default_schema.NotificationType import com.terraformation.backend.db.default_schema.OrganizationId import com.terraformation.backend.db.default_schema.ProjectId import com.terraformation.backend.db.forMultiset +import com.terraformation.backend.db.tracking.MonitoringPlotHistoryId import com.terraformation.backend.db.tracking.MonitoringPlotId import com.terraformation.backend.db.tracking.PlantingSeasonId import com.terraformation.backend.db.tracking.PlantingSiteHistoryId @@ -35,6 +36,7 @@ import com.terraformation.backend.db.tracking.tables.records.PlantingSiteHistori import com.terraformation.backend.db.tracking.tables.records.PlantingSubzoneHistoriesRecord import com.terraformation.backend.db.tracking.tables.records.PlantingZoneHistoriesRecord import com.terraformation.backend.db.tracking.tables.references.MONITORING_PLOTS +import com.terraformation.backend.db.tracking.tables.references.MONITORING_PLOT_HISTORIES import com.terraformation.backend.db.tracking.tables.references.OBSERVATION_PLOTS import com.terraformation.backend.db.tracking.tables.references.PLANTINGS import com.terraformation.backend.db.tracking.tables.references.PLANTING_SEASONS @@ -43,6 +45,7 @@ import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITE_HI import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITE_NOTIFICATIONS import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITE_POPULATIONS import com.terraformation.backend.db.tracking.tables.references.PLANTING_SUBZONES +import com.terraformation.backend.db.tracking.tables.references.PLANTING_SUBZONE_HISTORIES import com.terraformation.backend.db.tracking.tables.references.PLANTING_SUBZONE_POPULATIONS import com.terraformation.backend.db.tracking.tables.references.PLANTING_ZONES import com.terraformation.backend.db.tracking.tables.references.PLANTING_ZONE_POPULATIONS @@ -711,13 +714,18 @@ class PlantingSiteStore( } } + insertPlantingSubzoneHistory(edit.desiredModel, plantingZoneHistoryId, plantingSubzoneId) + edit.existingModel.monitoringPlots.forEach { plot -> if (plot.boundary.intersects(edit.removedRegion)) { replacementResults.add(makePlotUnavailable(plot.id)) } - } - insertPlantingSubzoneHistory(edit.desiredModel, plantingZoneHistoryId, plantingSubzoneId) + // For now, plots can't move between subzones, so we can add history for all the existing + // plots with the new site and subzone history IDs. + insertMonitoringPlotHistory( + plot.id, plantingSiteId, plantingSubzoneId, plot.name, plot.fullName) + } } } @@ -1507,6 +1515,8 @@ class PlantingSiteStore( monitoringPlotsDao.insert(monitoringPlotsRow) + insertMonitoringPlotHistory(monitoringPlotsRow) + monitoringPlotsRow.id!! } } @@ -1552,6 +1562,8 @@ class PlantingSiteStore( sizeMeters = MONITORING_PLOT_SIZE_INT) monitoringPlotsDao.insert(monitoringPlotsRow) + insertMonitoringPlotHistory(monitoringPlotsRow) + monitoringPlotsRow.id!! } } @@ -1978,4 +1990,48 @@ class PlantingSiteStore( return historiesRecord.id!! } + + private fun insertMonitoringPlotHistory( + monitoringPlotsRow: MonitoringPlotsRow + ): MonitoringPlotHistoryId { + return with(monitoringPlotsRow) { + insertMonitoringPlotHistory(id!!, plantingSiteId!!, plantingSubzoneId, name!!, fullName!!) + } + } + + private fun insertMonitoringPlotHistory( + monitoringPlotId: MonitoringPlotId, + plantingSiteId: PlantingSiteId, + plantingSubzoneId: PlantingSubzoneId?, + name: String, + fullName: String, + ): MonitoringPlotHistoryId { + return with(MONITORING_PLOT_HISTORIES) { + dslContext + .insertInto(MONITORING_PLOT_HISTORIES) + .set(CREATED_BY, currentUser().userId) + .set(CREATED_TIME, clock.instant()) + .set(FULL_NAME, fullName) + .set(MONITORING_PLOT_ID, monitoringPlotId) + .set(NAME, name) + .set( + PLANTING_SITE_HISTORY_ID, + DSL.select(DSL.max(PLANTING_SITE_HISTORIES.ID)) + .from(PLANTING_SITE_HISTORIES) + .where(PLANTING_SITE_HISTORIES.PLANTING_SITE_ID.eq(plantingSiteId))) + .set(PLANTING_SITE_ID, plantingSiteId) + .set( + PLANTING_SUBZONE_HISTORY_ID, + if (plantingSubzoneId != null) { + DSL.select(DSL.max(PLANTING_SUBZONE_HISTORIES.ID)) + .from(PLANTING_SUBZONE_HISTORIES) + .where(PLANTING_SUBZONE_HISTORIES.PLANTING_SUBZONE_ID.eq(plantingSubzoneId)) + } else { + null + }) + .set(PLANTING_SUBZONE_ID, plantingSubzoneId) + .returning(ID) + .fetchOne(ID)!! + } + } } diff --git a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreApplyEditTest.kt b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreApplyEditTest.kt index fc4679ef0ca1..01fbc1488d37 100644 --- a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreApplyEditTest.kt +++ b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreApplyEditTest.kt @@ -3,8 +3,11 @@ package com.terraformation.backend.tracking.db.plantingSiteStore import com.terraformation.backend.db.tracking.PlantingSubzoneId import com.terraformation.backend.db.tracking.PlantingZoneId import com.terraformation.backend.db.tracking.tables.pojos.PlantingSiteHistoriesRow -import com.terraformation.backend.db.tracking.tables.pojos.PlantingSubzoneHistoriesRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingZoneHistoriesRow +import com.terraformation.backend.db.tracking.tables.records.MonitoringPlotHistoriesRecord +import com.terraformation.backend.db.tracking.tables.records.PlantingSubzoneHistoriesRecord +import com.terraformation.backend.db.tracking.tables.references.MONITORING_PLOT_HISTORIES +import com.terraformation.backend.db.tracking.tables.references.PLANTING_SUBZONE_HISTORIES import com.terraformation.backend.point import com.terraformation.backend.rectangle import com.terraformation.backend.tracking.db.PlantingSiteMapInvalidException @@ -640,27 +643,52 @@ internal class PlantingSiteStoreApplyEditTest : PlantingSiteStoreTest() { plantingSubzoneHistoriesDao.fetchByPlantingZoneHistoryId( *editedZoneHistories.map { it.id!! }.toTypedArray()) - assertEquals( - edited.plantingZones.sumOf { it.plantingSubzones.size }, - editedSubzoneHistories.size, - "Number of planting subzone histories from edit") - assertEquals( - edited.plantingZones - .flatMap { zone -> - zone.plantingSubzones.map { subzone -> - PlantingSubzoneHistoriesRow( - plantingZoneHistoryId = - editedZoneHistories.first { it.plantingZoneId == zone.id }.id, - plantingSubzoneId = subzone.id, - name = subzone.name, - fullName = subzone.fullName, - boundary = subzone.boundary, - ) - } + assertTableEquals( + edited.plantingZones.flatMap { zone -> + val plantingZoneHistoryId = + editedZoneHistories.first { it.plantingZoneId == zone.id }.id + zone.plantingSubzones.map { subzone -> + PlantingSubzoneHistoriesRecord( + plantingZoneHistoryId = plantingZoneHistoryId, + plantingSubzoneId = subzone.id, + name = subzone.name, + fullName = subzone.fullName, + boundary = subzone.boundary, + ) + } + }, + "Planting subzone histories from edit", + where = + PLANTING_SUBZONE_HISTORIES.plantingZoneHistories.PLANTING_SITE_HISTORY_ID.eq( + editedSiteHistory.id)) + + val expectedPlotHistories = + edited.plantingZones.flatMap { zone -> + zone.plantingSubzones.flatMap { subzone -> + val plantingSubzoneHistoryId = + editedSubzoneHistories.first { it.plantingSubzoneId == subzone.id }.id + subzone.monitoringPlots.map { plot -> + MonitoringPlotHistoriesRecord( + createdBy = user.userId, + createdTime = editTime, + fullName = plot.fullName, + monitoringPlotId = plot.id, + name = plot.name, + plantingSiteHistoryId = editedSiteHistory.id, + plantingSiteId = edited.id, + plantingSubzoneHistoryId = plantingSubzoneHistoryId, + plantingSubzoneId = subzone.id, + ) } - .toSet(), - editedSubzoneHistories.map { it.copy(id = null) }.toSet(), - "Planting subzone histories from edit") + } + } + + if (expectedPlotHistories.isNotEmpty()) { + assertTableEquals( + expectedPlotHistories, + "Monitoring plot histories from edit", + where = MONITORING_PLOT_HISTORIES.PLANTING_SITE_HISTORY_ID.eq(editedSiteHistory.id)) + } } } diff --git a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateSiteTest.kt b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateSiteTest.kt index 7938fdacd3b3..ceb1d04d0f6a 100644 --- a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateSiteTest.kt +++ b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateSiteTest.kt @@ -9,6 +9,7 @@ import com.terraformation.backend.db.tracking.tables.pojos.PlantingSubzoneHistor import com.terraformation.backend.db.tracking.tables.pojos.PlantingSubzonesRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingZoneHistoriesRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingZonesRow +import com.terraformation.backend.db.tracking.tables.references.MONITORING_PLOT_HISTORIES import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITE_HISTORIES import com.terraformation.backend.db.tracking.tables.references.PLANTING_ZONES import com.terraformation.backend.point @@ -411,6 +412,8 @@ internal class PlantingSiteStoreCreateSiteTest : PlantingSiteStoreTest() { ), plantingSubzoneHistoriesDao.findAll().map { it.copy(id = null) }, "Planting subzone histories") + + assertTableEmpty(MONITORING_PLOT_HISTORIES) } @Test diff --git a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateTemporaryTest.kt b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateTemporaryTest.kt index f3e599923202..e57659b27a60 100644 --- a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateTemporaryTest.kt +++ b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreCreateTemporaryTest.kt @@ -1,10 +1,13 @@ package com.terraformation.backend.tracking.db.plantingSiteStore +import com.terraformation.backend.assertGeometryEquals import com.terraformation.backend.db.tracking.tables.pojos.MonitoringPlotsRow +import com.terraformation.backend.db.tracking.tables.records.MonitoringPlotHistoriesRecord import com.terraformation.backend.point import com.terraformation.backend.tracking.model.MONITORING_PLOT_SIZE_INT import com.terraformation.backend.util.Turtle import io.mockk.every +import java.time.Instant import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -17,9 +20,12 @@ internal class PlantingSiteStoreCreateTemporaryTest : PlantingSiteStoreTest() { @Test fun `creates new plot with correct details`() { val siteBoundary = Turtle(point(0)).makeMultiPolygon { square(51) } - val plantingSiteId = insertPlantingSite(boundary = siteBoundary, gridOrigin = point(0)) + val plantingSiteId = + insertPlantingSite(boundary = siteBoundary, gridOrigin = point(0), insertHistory = false) + val plantingSiteHistoryId = insertPlantingSiteHistory() val plantingZoneId = insertPlantingZone(boundary = siteBoundary) - val plantingSubzoneId = insertPlantingSubzone(boundary = siteBoundary) + val plantingSubzoneId = insertPlantingSubzone(boundary = siteBoundary, insertHistory = false) + val plantingSubzoneHistoryId = insertPlantingSubzoneHistory() insertMonitoringPlot( boundary = @@ -27,7 +33,8 @@ internal class PlantingSiteStoreCreateTemporaryTest : PlantingSiteStoreTest() { north(25) square(25) }, - name = "17") + name = "17", + insertHistory = false) val plotBoundary = Turtle(point(0)).makePolygon { square(25) } val newPlotId = store.createTemporaryPlot(plantingSiteId, plantingZoneId, plotBoundary) @@ -50,12 +57,21 @@ internal class PlantingSiteStoreCreateTemporaryTest : PlantingSiteStoreTest() { val actual = monitoringPlotsDao.fetchOneById(newPlotId)!! assertEquals(expected, actual.copy(boundary = null)) + assertGeometryEquals(plotBoundary, actual.boundary, "Plot boundary") - // PostGIS geometry representation isn't identical to GeoTools in-memory representation; do a - // fuzzy comparison with a very small tolerance. - if (!plotBoundary.equalsExact(actual.boundary!!, 0.00000000001)) { - assertEquals(plotBoundary, actual.boundary!!, "Plot boundary") - } + assertTableEquals( + listOf( + MonitoringPlotHistoriesRecord( + createdBy = user.userId, + createdTime = Instant.EPOCH, + fullName = "Z1-1-18", + monitoringPlotId = newPlotId, + name = "18", + plantingSiteHistoryId = plantingSiteHistoryId, + plantingSiteId = plantingSiteId, + plantingSubzoneHistoryId = plantingSubzoneHistoryId, + plantingSubzoneId = plantingSubzoneId, + ))) } @Test diff --git a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreEnsurePermanentTest.kt b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreEnsurePermanentTest.kt index 3761064ec0e0..b2db18067719 100644 --- a/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreEnsurePermanentTest.kt +++ b/src/test/kotlin/com/terraformation/backend/tracking/db/plantingSiteStore/PlantingSiteStoreEnsurePermanentTest.kt @@ -1,8 +1,10 @@ package com.terraformation.backend.tracking.db.plantingSiteStore +import com.terraformation.backend.db.tracking.tables.records.MonitoringPlotHistoriesRecord import com.terraformation.backend.point import com.terraformation.backend.tracking.model.MONITORING_PLOT_SIZE import com.terraformation.backend.util.Turtle +import java.time.Instant import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -14,9 +16,12 @@ internal class PlantingSiteStoreEnsurePermanentTest : PlantingSiteStoreTest() { fun `creates all clusters in empty planting site`() { val siteBoundary = Turtle(point(0)).makeMultiPolygon { square(101) } - val plantingSiteId = insertPlantingSite(boundary = siteBoundary, gridOrigin = point(0)) + val plantingSiteId = + insertPlantingSite(boundary = siteBoundary, gridOrigin = point(0), insertHistory = false) + val plantingSiteHistoryId = insertPlantingSiteHistory() insertPlantingZone(boundary = siteBoundary, numPermanentClusters = 4) - insertPlantingSubzone(boundary = siteBoundary) + val plantingSubzoneId = insertPlantingSubzone(boundary = siteBoundary, insertHistory = false) + val plantingSubzoneHistoryId = insertPlantingSubzoneHistory() store.ensurePermanentClustersExist(plantingSiteId) @@ -27,6 +32,21 @@ internal class PlantingSiteStoreEnsurePermanentTest : PlantingSiteStoreTest() { setOf(1, 2, 3, 4), plots.map { it.permanentCluster }.toSet(), "Permanent cluster numbers") assertEquals(1, plots.minOf { it.name!!.toInt() }, "Smallest plot number") assertEquals(4, plots.maxOf { it.name!!.toInt() }, "Largest plot number") + + assertTableEquals( + plots.map { plot -> + MonitoringPlotHistoriesRecord( + createdBy = user.userId, + createdTime = Instant.EPOCH, + fullName = plot.fullName, + monitoringPlotId = plot.id, + name = plot.name, + plantingSiteHistoryId = plantingSiteHistoryId, + plantingSiteId = plantingSiteId, + plantingSubzoneHistoryId = plantingSubzoneHistoryId, + plantingSubzoneId = plantingSubzoneId, + ) + }) } @Test @@ -58,11 +78,18 @@ internal class PlantingSiteStoreEnsurePermanentTest : PlantingSiteStoreTest() { val siteBoundary = Turtle(gridOrigin).makeMultiPolygon { square(201) } val existingPlotBoundary = Turtle(gridOrigin).makePolygon { square(25) } - val plantingSiteId = insertPlantingSite(boundary = siteBoundary, gridOrigin = gridOrigin) + val plantingSiteId = + insertPlantingSite( + boundary = siteBoundary, gridOrigin = gridOrigin, insertHistory = false) + val plantingSiteHistoryId = insertPlantingSiteHistory() insertPlantingZone(boundary = siteBoundary, numPermanentClusters = 3) - insertPlantingSubzone(boundary = siteBoundary) + val plantingSubzoneId = insertPlantingSubzone(boundary = siteBoundary, insertHistory = false) + val plantingSubzoneHistoryId = insertPlantingSubzoneHistory() insertMonitoringPlot( - boundary = existingPlotBoundary, permanentCluster = 2, permanentClusterSubplot = 1) + boundary = existingPlotBoundary, + permanentCluster = 2, + permanentClusterSubplot = 1, + insertHistory = false) store.ensurePermanentClustersExist(plantingSiteId) @@ -71,6 +98,23 @@ internal class PlantingSiteStoreEnsurePermanentTest : PlantingSiteStoreTest() { assertEquals(3, plots.size, "Number of monitoring plots including existing one") assertEquals( setOf(1, 2, 3), plots.map { it.permanentCluster }.toSet(), "Permanent cluster numbers") + + assertTableEquals( + plots + .filter { it.permanentCluster != 2 } + .map { plot -> + MonitoringPlotHistoriesRecord( + createdBy = user.userId, + createdTime = Instant.EPOCH, + fullName = plot.fullName, + monitoringPlotId = plot.id, + name = plot.name, + plantingSiteHistoryId = plantingSiteHistoryId, + plantingSiteId = plantingSiteId, + plantingSubzoneHistoryId = plantingSubzoneHistoryId, + plantingSubzoneId = plantingSubzoneId, + ) + }) } @Test