Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 3 additions & 1 deletion db/notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def notes(cls):
cascade="all, delete-orphan",
lazy="selectin",
overlaps="notes",
order_by=Notes.created_at,
)

def add_note(
Expand All @@ -125,4 +126,5 @@ def add_note(
)

def _get_notes(self, note_type: str) -> list[Notes]:
return [n for n in self.notes if n.note_type == note_type]
notes = [n for n in self.notes if n.note_type == note_type]
return sorted(notes, key=lambda n: n.created_at)
34 changes: 30 additions & 4 deletions db/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,18 @@ def current_location(self):
else None
)

@property
def site_name(self) -> str | None:
nmbgmr_link = next(
(
link
for link in sorted(self.links, key=lambda link: link.id)
if link.alternate_organization == "NMBGMR"
),
None,
)
return nmbgmr_link.alternate_id if nmbgmr_link is not None else None

@property
def water_notes(self):
return self._get_notes("Water")
Expand All @@ -438,6 +450,14 @@ def construction_notes(self):
def site_notes(self):
return self._get_notes("Site Notes (legacy)")

@property
def historic_depth_to_water(self) -> list[str]:
return [note.content for note in self._get_notes("Historical")]

@property
def well_location_note(self) -> list[str]:
return [note.content for note in self._get_notes("Access")]
Comment thread
jirhiker marked this conversation as resolved.

@property
def well_status(self) -> str | None:
"""
Expand Down Expand Up @@ -465,17 +485,23 @@ def monitoring_status(self) -> str | None:
return latest_status.status_value if latest_status else None

@property
def open_status(self) -> str | None:
def open_status(self) -> bool | None:
"""
Returns the open status from the most recent status history entry
where status_type is "Open Status".
Returns the open status as a boolean derived from the most recent
"Open Status" history entry.

Since status_history is eagerly loaded, this should not introduce N+1 query issues.
"""
latest_status = retrieve_latest_polymorphic_history_table_record(
self, "status_history", "Open Status"
)
return latest_status.status_value if latest_status else None
if latest_status is None:
return None
if latest_status.status_value == "Open":
return True
if latest_status.status_value == "Closed":
return False
return None

@property
def datalogger_suitability_status(self) -> str | None:
Expand Down
8 changes: 8 additions & 0 deletions schemas/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from schemas import BaseResponseModel
from core.enums import ActivityType
from schemas.contact import ContactResponse

# RESPONSE ---------------------------------------------------------------------

Expand All @@ -15,3 +16,10 @@ class FieldEventResponse(BaseResponseModel):
thing_id: int
event_date: AwareDatetime
notes: str | None


class FieldEventParticipantResponse(BaseResponseModel):
field_event_id: int
contact_id: int
participant_role: str
participant: ContactResponse
2 changes: 2 additions & 0 deletions schemas/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from core.constants import SRID_WGS84, SRID_UTM_ZONE_13N
from core.enums import ElevationMethod, CoordinateMethod
from core.enums import ReleaseStatus
from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel
from schemas.notes import NoteResponse, CreateNote, UpdateNote
from services.util import convert_m_to_ft, transform_srid
Expand Down Expand Up @@ -122,6 +123,7 @@ class GeoJSONProperties(BaseModel):

class LocationGeoJSONResponse(BaseModel):
type: str = "Feature"
release_status: ReleaseStatus
geometry: GeoJSONGeometry
properties: GeoJSONProperties

Expand Down
5 changes: 4 additions & 1 deletion schemas/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class MonitoringFrequencyResponse(BaseModel):

class BaseThingResponse(BaseResponseModel):
name: str
site_name: str | None = None
thing_type: str
current_location: LocationGeoJSONResponse
first_visit_date: PastOrTodayDate | None
Expand Down Expand Up @@ -247,6 +248,7 @@ class WellResponse(BaseThingResponse):
well_depth: float | None = None
well_depth_unit: str = "ft"
well_depth_source: str | None
historic_depth_to_water: list[str] = []
hole_depth: float | None = None
hole_depth_unit: str = "ft"
well_casing_diameter: float | None = None # in inches
Expand All @@ -263,7 +265,7 @@ class WellResponse(BaseThingResponse):
well_pump_depth: float | None
well_pump_depth_unit: str = "ft"
well_status: str | None
open_status: str | None
open_status: bool | None
datalogger_suitability_status: str | None
measuring_point_height: float | None
measuring_point_height_unit: str = "ft"
Expand All @@ -275,6 +277,7 @@ class WellResponse(BaseThingResponse):
permissions: list[PermissionHistoryResponse]
formation_completion_code: FormationCode | None
nma_formation_zone: str | None
well_location_note: list[str] = []

@field_validator("well_purposes", mode="before")
def populate_well_purposes_with_strings(cls, well_purposes):
Expand Down
4 changes: 4 additions & 0 deletions schemas/well_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from schemas.deployment import DeploymentResponse
from schemas.observation import GroundwaterLevelObservationResponse
from schemas.sample import SampleResponse
from schemas.field import FieldEventParticipantResponse
from schemas.sensor import SensorResponse
from schemas.thing import WellResponse, WellScreenResponse

Expand All @@ -20,3 +21,6 @@ class WellDetailsResponse(BaseModel):
Field(default_factory=list)
)
latest_field_event_sample: SampleResponse | None = None
field_event_participants: list[FieldEventParticipantResponse] = Field(
default_factory=list
)
9 changes: 9 additions & 0 deletions services/well_details_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ def get_well_details_payload(
joinedload(Sample.field_activity)
.joinedload(FieldActivity.field_event)
.joinedload(FieldEvent.thing),
joinedload(Sample.field_activity)
.joinedload(FieldActivity.field_event)
.selectinload(FieldEvent.field_event_participants)
.selectinload(FieldEventParticipant.participant),
joinedload(Sample.field_event_participant).joinedload(
FieldEventParticipant.participant
),
Expand Down Expand Up @@ -186,6 +190,11 @@ def get_well_details_payload(
"well_screens": well_screens,
"recent_groundwater_level_observations": recent_groundwater_level_observations,
"latest_field_event_sample": latest_field_event_sample,
"field_event_participants": (
latest_field_event_sample.field_event.field_event_participants
if latest_field_event_sample is not None
else []
),
}


Expand Down
160 changes: 140 additions & 20 deletions tests/test_thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
viewer_function,
amp_viewer_function,
)
from db import MeasuringPointHistory, Thing, ThingIdLink, WellScreen
from db import MeasuringPointHistory, StatusHistory, Thing, ThingIdLink, WellScreen
from db.engine import session_ctx
from main import app
from schemas import DT_FMT
Expand Down Expand Up @@ -563,6 +563,7 @@ def test_get_water_well_by_id(water_well_thing, location):
def test_get_water_well_details_payload(
water_well_thing,
field_event,
field_event_participant,
contact,
email,
phone,
Expand All @@ -574,28 +575,87 @@ def test_get_water_well_details_payload(
groundwater_level_sample,
groundwater_level_observation,
):
with session_ctx() as session:
well = session.get(type(water_well_thing), water_well_thing.id)
session.add(well.add_note("historic depth to water: 12 ft", "Historical"))
session.add(well.add_note("historic depth to water: 18 ft", "Historical"))
session.add(well.add_note("turn left at the cattle guard", "Access"))
session.add(well.add_note("use the south gate", "Access"))
location = well.current_location
location.release_status = "private"

second_contact = contact.__class__(
name="Second Participant",
organization=None,
role=contact.role,
contact_type=contact.contact_type,
release_status="draft",
)
session.add(second_contact)
session.flush()
second_participant = field_event_participant.__class__(
field_event_id=field_event.id,
contact_id=second_contact.id,
participant_role="Lead",
)
session.add(second_participant)
session.commit()
second_contact_id = second_contact.id
second_participant_id = second_participant.id

response = client.get(f"/thing/water-well/{water_well_thing.id}/details")

assert response.status_code == 200
data = response.json()
try:
assert response.status_code == 200
data = response.json()

assert data["well"]["id"] == water_well_thing.id
assert data["well"]["alternate_ids"][0]["id"] == thing_id_link.id
assert data["contacts"][0]["id"] == contact.id
assert data["contacts"][0]["emails"][0]["id"] == email.id
assert data["contacts"][0]["phones"][0]["id"] == phone.id
assert data["contacts"][0]["addresses"][0]["id"] == address.id
assert data["sensors"][0]["id"] == sensor.id
assert data["deployments"][0]["id"] == sensor_to_water_well_thing_deployment.id
assert data["deployments"][0]["sensor"]["id"] == sensor.id
assert data["well_screens"][0]["id"] == well_screen.id
assert (
data["recent_groundwater_level_observations"][0]["id"]
== groundwater_level_observation.id
)
assert data["latest_field_event_sample"]["id"] == groundwater_level_sample.id
assert data["latest_field_event_sample"]["field_event"]["id"] == field_event.id
assert data["latest_field_event_sample"]["contact"]["id"] == contact.id
assert data["well"]["id"] == water_well_thing.id
assert data["well"]["alternate_ids"][0]["id"] == thing_id_link.id
assert data["well"]["historic_depth_to_water"] == [
"historic depth to water: 12 ft",
"historic depth to water: 18 ft",
]
assert data["well"]["well_location_note"] == [
"turn left at the cattle guard",
"use the south gate",
]
assert data["well"]["current_location"]["release_status"] == "private"
assert data["contacts"][0]["id"] == contact.id
assert data["contacts"][0]["emails"][0]["id"] == email.id
assert data["contacts"][0]["phones"][0]["id"] == phone.id
assert data["contacts"][0]["addresses"][0]["id"] == address.id
assert data["sensors"][0]["id"] == sensor.id
assert data["deployments"][0]["id"] == sensor_to_water_well_thing_deployment.id
assert data["deployments"][0]["sensor"]["id"] == sensor.id
assert data["well_screens"][0]["id"] == well_screen.id
assert (
data["recent_groundwater_level_observations"][0]["id"]
== groundwater_level_observation.id
)
assert data["latest_field_event_sample"]["id"] == groundwater_level_sample.id
assert data["latest_field_event_sample"]["field_event"]["id"] == field_event.id
assert data["latest_field_event_sample"]["contact"]["id"] == contact.id
assert {
participant["id"] for participant in data["field_event_participants"]
} == {
field_event_participant.id,
second_participant_id,
}
assert {
participant["participant"]["id"]
for participant in data["field_event_participants"]
} == {contact.id, second_contact_id}
finally:
with session_ctx() as session:
second_participant = session.get(
field_event_participant.__class__, second_participant_id
)
if second_participant is not None:
session.delete(second_participant)
second_contact = session.get(contact.__class__, second_contact_id)
if second_contact is not None:
session.delete(second_contact)
session.commit()


def test_get_water_well_details_payload_uses_latest_observation_sample(
Expand Down Expand Up @@ -681,6 +741,66 @@ def test_get_water_well_by_id_includes_location_properties(
assert data["current_location"]["properties"]["quad_name"] == "Hillsboro Peak"


def test_get_water_well_by_id_includes_site_name_from_nmbgmr_link(
water_well_thing,
thing_id_link,
):
with session_ctx() as session:
site_name_link = ThingIdLink(
thing_id=water_well_thing.id,
relation="same_as",
alternate_id="John Smith Well",
alternate_organization="NMBGMR",
release_status="private",
)
session.add(site_name_link)
session.commit()
site_name_link_id = site_name_link.id

try:
response = client.get(f"/thing/water-well/{water_well_thing.id}")

assert response.status_code == 200
data = response.json()
assert data["site_name"] == "John Smith Well"
finally:
with session_ctx() as session:
site_name_link = session.get(ThingIdLink, site_name_link_id)
if site_name_link is not None:
session.delete(site_name_link)
session.commit()


def test_get_water_well_by_id_includes_open_status_boolean(water_well_thing):
with session_ctx() as session:
open_status = StatusHistory(
status_type="Open Status",
status_value="Open",
start_date=date(2025, 1, 1),
end_date=None,
reason="test open status",
target_id=water_well_thing.id,
target_table="thing",
release_status="draft",
)
session.add(open_status)
session.commit()
open_status_id = open_status.id

try:
response = client.get(f"/thing/water-well/{water_well_thing.id}")

assert response.status_code == 200
data = response.json()
assert data["open_status"] is True
finally:
with session_ctx() as session:
open_status = session.get(StatusHistory, open_status_id)
if open_status is not None:
session.delete(open_status)
session.commit()


def test_get_water_wells_includes_contact_summary(
water_well_thing,
contact,
Expand Down
Loading
Loading