Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combined functionality to schedule and complete ad-hoc observation into one endpoint #2697

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.terraformation.backend.tracking

import com.terraformation.backend.customer.db.ParentStore
import com.terraformation.backend.customer.model.SystemUser
import com.terraformation.backend.customer.model.requirePermissions
import com.terraformation.backend.db.FileNotFoundException
import com.terraformation.backend.db.asNonNullable
import com.terraformation.backend.db.default_schema.FileId
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.ObservationState
Expand All @@ -14,6 +16,7 @@ import com.terraformation.backend.db.tracking.PlantingSiteId
import com.terraformation.backend.db.tracking.tables.daos.MonitoringPlotsDao
import com.terraformation.backend.db.tracking.tables.daos.ObservationPhotosDao
import com.terraformation.backend.db.tracking.tables.pojos.ObservationPhotosRow
import com.terraformation.backend.db.tracking.tables.pojos.RecordedPlantsRow
import com.terraformation.backend.db.tracking.tables.references.OBSERVATION_PHOTOS
import com.terraformation.backend.file.FileService
import com.terraformation.backend.file.SizedInputStream
Expand Down Expand Up @@ -67,6 +70,7 @@ class ObservationService(
private val observationStore: ObservationStore,
private val plantingSiteStore: PlantingSiteStore,
private val parentStore: ParentStore,
private val systemUser: SystemUser,
) {
private val log = perClassLogger()

Expand Down Expand Up @@ -408,38 +412,56 @@ class ObservationService(
}

/**
* Schedule an ad-hoc observation. This creates an ad-hoc observation, creates an ad-hoc
* monitoring plot, and adds the plot to the observation.
* Record an ad-hoc observation. This creates an ad-hoc observation, creates an ad-hoc monitoring
tommylau523 marked this conversation as resolved.
Show resolved Hide resolved
* plot, adds the plot to the observation, and complete the observation with plants.
*/
fun scheduleAdHocObservation(
endDate: LocalDate,
fun completeAdHocObservation(
conditions: Set<ObservableCondition>,
notes: String?,
observedTime: Instant,
observationType: ObservationType,
plantingSiteId: PlantingSiteId,
plants: Collection<RecordedPlantsRow>,
plotName: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that plot names are going away shortly, so we shouldn't build new client code that expects to be able to pass in a user-supplied name. (But removing this parameter isn't appropriate yet since names still exist for now.)

startDate: LocalDate,
swCorner: Point,
): Pair<ObservationId, MonitoringPlotId> {
requirePermissions { scheduleAdHocObservation(plantingSiteId) }

validateSchedule(plantingSiteId, startDate, endDate)
if (observedTime.isAfter(clock.instant())) {
tommylau523 marked this conversation as resolved.
Show resolved Hide resolved
throw IllegalArgumentException("Observed time is in the future")
}

val effectiveTimeZone = parentStore.getEffectiveTimeZone(plantingSiteId)
val date = LocalDate.ofInstant(observedTime, effectiveTimeZone)

return dslContext.transactionResult { _ ->
val observationId =
observationStore.createObservation(
NewObservationModel(
endDate = endDate,
endDate = date,
id = null,
isAdHoc = true,
observationType = observationType,
plantingSiteId = plantingSiteId,
requestedSubzoneIds = emptySet(),
startDate = startDate,
startDate = date,
state = ObservationState.Upcoming))

val plotId = plantingSiteStore.createAdHocMonitoringPlot(plotName, plantingSiteId, swCorner)

observationStore.addAdHocPlotToObservation(observationId, plotId)

systemUser.run { observationStore.recordObservationStart(observationId) }

observationStore.claimPlot(observationId, plotId)
observationStore.completePlot(
observationId,
plotId,
conditions,
notes,
observedTime,
plants,
)

observationId to plotId
}
}
Expand Down Expand Up @@ -527,9 +549,9 @@ class ObservationService(
}

/**
* Validation rules:
* 1. start date can be up to one year from today and not earlier than today
* 2. end date should be after the start date but no more than 2 months from the start date
* Validation rules: 1a. for non-ad-hoc, start date can be up to one year from today and not
* earlier than today. 1b. for ad-hoc, start date can be on or before today.
* 2. end date should be after the start date but no more than 2 months from the start date.
tommylau523 marked this conversation as resolved.
Show resolved Hide resolved
*/
private fun validateSchedule(
plantingSiteId: PlantingSiteId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,22 +395,24 @@ class ObservationsController(
return SimpleSuccessResponsePayload()
}

@Operation(summary = "Schedules a new ad-hoc observation.")
@Operation(summary = "Records a new completed ad-hoc observation.")
@PostMapping("/adHoc")
fun scheduleAdHocObservation(
@RequestBody payload: ScheduleAdHocObservationRequestPayload
): ScheduleAdHocObservationResponsePayload {
val (observationId) =
observationService.scheduleAdHocObservation(
payload.endDate,
payload.type,
fun completeAdHocObservation(
@RequestBody payload: CompleteAdHocObservationRequestPayload
): CompleteAdHocObservationResponsePayload {
val (observationId, plotId) =
observationService.completeAdHocObservation(
payload.conditions,
payload.notes,
payload.observedTime,
payload.observationType,
payload.plantingSiteId,
payload.plants.map { it.toRow() },
payload.plotName,
payload.startDate,
payload.swCorner,
)

return ScheduleAdHocObservationResponsePayload(observationId)
return CompleteAdHocObservationResponsePayload(observationId, plotId)
}

@Operation(summary = "Schedules a new observation.")
Expand Down Expand Up @@ -938,6 +940,21 @@ data class PlantingSiteObservationSummaryPayload(
plantingZones = model.plantingZones.map { PlantingZoneObservationSummaryPayload(it) })
}

data class CompleteAdHocObservationRequestPayload(
val conditions: Set<ObservableCondition>,
val notes: String?,
@Schema(description = "Date and time the observation was performed in the field.")
val observedTime: Instant,
@Schema(description = "Observation type for this observation.")
val observationType: ObservationType,
@Schema(description = "The plot name for the ad-hoc plot.") val plotName: String,
val plants: List<RecordedPlantPayload>,
@Schema(description = "Which planting site this observation needs to be scheduled for.")
val plantingSiteId: PlantingSiteId,
@Schema(description = "GPS coordinates for the South West corner of the ad-hoc plot.")
val swCorner: Point,
)

data class CompletePlotObservationRequestPayload(
val conditions: Set<ObservableCondition>,
val notes: String?,
Expand Down Expand Up @@ -1011,24 +1028,6 @@ data class GetPlantingSiteObservationSummariesPayload(
val summaries: List<PlantingSiteObservationSummaryPayload>,
) : SuccessResponsePayload

data class ScheduleAdHocObservationRequestPayload(
@Schema(
description =
"The end date for this observation, should be limited to 2 months from the start date.")
val endDate: LocalDate,
@Schema(description = "The plot name for the ad-hoc plot.") val plotName: String,
@Schema(description = "Which planting site this observation needs to be scheduled for.")
val plantingSiteId: PlantingSiteId,
@Schema(
description =
"The start date for this observation, can be up to a year from the date this " +
"schedule request occurs on.")
val startDate: LocalDate,
@Schema(description = "GPS coordinates for the South West corner of the ad-hoc plot.")
val swCorner: Point,
@Schema(description = "Observation type for this observation.") val type: ObservationType,
)

data class ScheduleObservationRequestPayload(
@Schema(
description =
Expand Down Expand Up @@ -1059,7 +1058,10 @@ data class ScheduleObservationRequestPayload(
state = ObservationState.Upcoming)
}

data class ScheduleAdHocObservationResponsePayload(val id: ObservationId) : SuccessResponsePayload
data class CompleteAdHocObservationResponsePayload(
val observationId: ObservationId,
val plotId: MonitoringPlotId
) : SuccessResponsePayload

data class ScheduleObservationResponsePayload(val id: ObservationId) : SuccessResponsePayload

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,8 @@ class ObservationStore(
val (plantingZoneId, plantingSiteId) =
dslContext
.select(
MONITORING_PLOTS.plantingSubzones.PLANTING_ZONE_ID.asNonNullable(),
MONITORING_PLOTS.plantingSubzones.PLANTING_SITE_ID.asNonNullable())
MONITORING_PLOTS.plantingSubzones.PLANTING_ZONE_ID,
MONITORING_PLOTS.PLANTING_SITE_ID.asNonNullable())
.from(MONITORING_PLOTS)
.where(MONITORING_PLOTS.ID.eq(monitoringPlotId))
.fetchOne()!!
Expand Down Expand Up @@ -1359,7 +1359,7 @@ class ObservationStore(
private fun updateSpeciesTotals(
observationId: ObservationId,
plantingSiteId: PlantingSiteId,
plantingZoneId: PlantingZoneId,
plantingZoneId: PlantingZoneId?,
monitoringPlotId: MonitoringPlotId?,
isPermanent: Boolean,
plantCountsBySpecies: Map<RecordedSpeciesKey, Map<RecordedPlantStatus, Int>>
Expand All @@ -1374,13 +1374,17 @@ class ObservationStore(
plantCountsBySpecies,
)
}
updateSpeciesTotalsTable(
OBSERVED_ZONE_SPECIES_TOTALS.PLANTING_ZONE_ID,
observationId,
plantingZoneId,
isPermanent,
plantCountsBySpecies,
)

if (plantingZoneId != null) {
updateSpeciesTotalsTable(
OBSERVED_ZONE_SPECIES_TOTALS.PLANTING_ZONE_ID,
observationId,
plantingZoneId,
isPermanent,
plantCountsBySpecies,
)
}

updateSpeciesTotalsTable(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verifying the intent here: we want to update the site-level species totals based on ad-hoc observations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly due to how observation result store works. Since it relies on a plantingSiteSpeciesMultiset to get a lot of the data.

I was thinking that, since ad-hoc plots are always temporary, it will not have permanent data so it should not be so consequential. But perhaps it is easier to write new queries to deal with ad-hoc plots on the result side.

OBSERVED_SITE_SPECIES_TOTALS.PLANTING_SITE_ID,
observationId,
Expand Down
Loading