-
Notifications
You must be signed in to change notification settings - Fork 4
Add consolidated water well details endpoint for the well show page #622
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,7 @@ | |
| UpdateThingIdLink, | ||
| UpdateWellScreen, | ||
| ) | ||
| from schemas.well_details import WellDetailsResponse | ||
| from services.crud_helper import model_patcher, model_adder, model_deleter | ||
| from services.exceptions_helper import PydanticStyleException | ||
| from services.lexicon_helper import get_terms_by_category | ||
|
|
@@ -68,6 +69,7 @@ | |
| modify_well_descriptor_tables, | ||
| WELL_DESCRIPTOR_MODEL_MAP, | ||
| ) | ||
| from services.well_details_helper import get_well_details_payload | ||
|
|
||
| router = APIRouter(prefix="/thing", tags=["thing"]) | ||
|
|
||
|
|
@@ -177,6 +179,28 @@ async def get_well_by_id( | |
| return get_thing_of_a_thing_type_by_id(session, request, thing_id) | ||
|
|
||
|
|
||
| @router.get( | ||
| "/water-well/{thing_id}/details", | ||
| summary="Get water well details payload", | ||
| status_code=HTTP_200_OK, | ||
| ) | ||
| async def get_well_details( | ||
| user: viewer_dependency, | ||
| thing_id: int, | ||
| session: session_dependency, | ||
| request: Request, | ||
| ) -> WellDetailsResponse: | ||
| """ | ||
| Retrieve the consolidated payload needed to render the well details page. | ||
| Hydrograph series and map layer loading are intentionally handled separately. | ||
| """ | ||
| return get_well_details_payload( | ||
| session=session, | ||
| request=request, | ||
| thing_id=thing_id, | ||
| ) | ||
|
Comment on lines
+182
to
+201
|
||
|
|
||
|
|
||
| @router.get( | ||
| "/water-well/{thing_id}/well-screen", | ||
| summary="Get well screens by water well ID", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from pydantic import BaseModel, ConfigDict, Field | ||
|
|
||
| from schemas.contact import ContactResponse | ||
| from schemas.deployment import DeploymentResponse | ||
| from schemas.observation import GroundwaterLevelObservationResponse | ||
| from schemas.sample import SampleResponse | ||
| from schemas.sensor import SensorResponse | ||
| from schemas.thing import WellResponse, WellScreenResponse | ||
|
|
||
|
|
||
| class WellDetailsResponse(BaseModel): | ||
| model_config = ConfigDict(from_attributes=True) | ||
|
|
||
| well: WellResponse | ||
| contacts: list[ContactResponse] = Field(default_factory=list) | ||
| sensors: list[SensorResponse] = Field(default_factory=list) | ||
| deployments: list[DeploymentResponse] = Field(default_factory=list) | ||
| well_screens: list[WellScreenResponse] = Field(default_factory=list) | ||
| recent_groundwater_level_observations: list[GroundwaterLevelObservationResponse] = ( | ||
|
Comment on lines
+18
to
+19
|
||
| Field(default_factory=list) | ||
| ) | ||
| latest_field_event_sample: SampleResponse | None = None | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,110 @@ | ||||||||||||
| from sqlalchemy import select | ||||||||||||
| from sqlalchemy.orm import Session, joinedload, selectinload | ||||||||||||
|
|
||||||||||||
| from db import ( | ||||||||||||
| Contact, | ||||||||||||
| Deployment, | ||||||||||||
| FieldActivity, | ||||||||||||
| FieldEvent, | ||||||||||||
| FieldEventParticipant, | ||||||||||||
| Observation, | ||||||||||||
| Parameter, | ||||||||||||
| Sample, | ||||||||||||
| Sensor, | ||||||||||||
| ThingContactAssociation, | ||||||||||||
| WellScreen, | ||||||||||||
| ) | ||||||||||||
| from services.thing_helper import get_thing_of_a_thing_type_by_id | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def get_well_details_payload( | ||||||||||||
| session: Session, | ||||||||||||
| request, | ||||||||||||
| thing_id: int, | ||||||||||||
| recent_observation_limit: int = 100, | ||||||||||||
| ): | ||||||||||||
| well = get_thing_of_a_thing_type_by_id(session, request, thing_id) | ||||||||||||
|
|
||||||||||||
| contacts = session.scalars( | ||||||||||||
| select(Contact) | ||||||||||||
| .join(ThingContactAssociation) | ||||||||||||
| .where(ThingContactAssociation.thing_id == well.id) | ||||||||||||
| .options( | ||||||||||||
| selectinload(Contact.emails), | ||||||||||||
| selectinload(Contact.phones), | ||||||||||||
| selectinload(Contact.addresses), | ||||||||||||
| selectinload(Contact.incomplete_nma_phones), | ||||||||||||
| selectinload(Contact.thing_associations).selectinload( | ||||||||||||
| ThingContactAssociation.thing | ||||||||||||
| ), | ||||||||||||
| ) | ||||||||||||
| .order_by(Contact.id) | ||||||||||||
| ).all() | ||||||||||||
|
|
||||||||||||
| sensors = session.scalars( | ||||||||||||
| select(Sensor) | ||||||||||||
| .join(Deployment) | ||||||||||||
| .where(Deployment.thing_id == well.id) | ||||||||||||
| .distinct() | ||||||||||||
| .order_by(Sensor.id) | ||||||||||||
| ).all() | ||||||||||||
|
|
||||||||||||
| deployments = session.scalars( | ||||||||||||
| select(Deployment) | ||||||||||||
| .where(Deployment.thing_id == well.id) | ||||||||||||
| .options(selectinload(Deployment.sensor)) | ||||||||||||
| .order_by(Deployment.installation_date.desc(), Deployment.id.desc()) | ||||||||||||
| ).all() | ||||||||||||
|
|
||||||||||||
| well_screens = session.scalars( | ||||||||||||
| select(WellScreen) | ||||||||||||
| .where(WellScreen.thing_id == well.id) | ||||||||||||
| .order_by(WellScreen.screen_depth_top.asc(), WellScreen.id.asc()) | ||||||||||||
| ).all() | ||||||||||||
|
|
||||||||||||
| groundwater_parameter_id = ( | ||||||||||||
| session.query(Parameter) | ||||||||||||
| .filter(Parameter.parameter_name == "groundwater level") | ||||||||||||
|
||||||||||||
| .filter(Parameter.parameter_name == "groundwater level") | |
| .filter( | |
| Parameter.parameter_name == "groundwater level", | |
| Parameter.matrix == "groundwater", | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disambiguate groundwater parameter query before calling one()
The lookup Parameter.parameter_name == "groundwater level" is not unique by schema (the table enforces uniqueness on (parameter_name, matrix)), so this can raise MultipleResultsFound and turn the whole details endpoint into a 500 as soon as another matrix/type variant with the same name is added. Filtering by matrix/type (or otherwise handling multiple matches) avoids a production-breaking failure tied to normal data growth.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This endpoint is guarded by
viewer_dependency, but it returnsrecent_groundwater_level_observationsandlatest_field_event_sample, which exposes groundwater-observation data through/thing/water-well/{thing_id}/detailsto users who may not have AMP permissions. In this repo, groundwater observation routes (for example,/observation/groundwater-levelinapi/observation.py) requireamp_viewer_dependency, so this change creates an authorization bypass for any principal that has Viewer but not AMPViewer.Useful? React with 👍 / 👎.