Skip to content
2 changes: 2 additions & 0 deletions api/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from fastapi_pagination.ext.sqlalchemy import paginate
from sqlalchemy import select
from sqlalchemy.exc import ProgrammingError
from sqlalchemy.orm import selectinload
from starlette.status import (
HTTP_200_OK,
HTTP_201_CREATED,
Expand Down Expand Up @@ -361,6 +362,7 @@ async def get_thing_deployments(
"""
thing = simple_get_by_id(session, Thing, thing_id)
sql = select(Deployment).where(Deployment.thing_id == thing.id)
sql = sql.options(selectinload(Deployment.sensor))
return paginate(query=sql, conn=session)


Expand Down
4 changes: 1 addition & 3 deletions db/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,4 @@ class Deployment(Base, AutoBaseMixin, ReleaseMixin):
# Many-To-One: A Deployment is for one Thing.
thing: Mapped["Thing"] = relationship("Thing", back_populates="deployments")
# Many-To-One: A Deployment is of one piece of equipment (sensor).
sensor: Mapped["Sensor"] = relationship(
"Sensor", back_populates="deployments", lazy="joined"
)
sensor: Mapped["Sensor"] = relationship("Sensor", back_populates="deployments")
4 changes: 1 addition & 3 deletions db/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,7 @@ class LocationThingAssociation(Base, AutoBaseMixin):
)

# --- Relationship Definitions ---
location: Mapped["Location"] = relationship(
back_populates="thing_associations", lazy="joined"
)
location: Mapped["Location"] = relationship(back_populates="thing_associations")
thing: Mapped["Thing"] = relationship(back_populates="location_associations")


Expand Down
2 changes: 1 addition & 1 deletion db/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin):

# Many-To-One: An Observation measures one Parameter.
parameter: Mapped["Parameter"] = relationship(
"Parameter", back_populates="observations", lazy="joined"
"Parameter", back_populates="observations", lazy="selectin"
)


Expand Down
11 changes: 0 additions & 11 deletions db/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ class Thing(
cascade="all, delete-orphan",
passive_deletes=True,
order_by="LocationThingAssociation.effective_start.desc()",
lazy="joined",
)

contact_associations = relationship(
Expand Down Expand Up @@ -254,23 +253,20 @@ class Thing(
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)

well_casing_materials: Mapped[List["WellCasingMaterial"]] = relationship(
"WellCasingMaterial",
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)

links: Mapped[List["ThingIdLink"]] = relationship(
"ThingIdLink",
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)

# One-To-Many: A Thing (well) can have multiple measuring points over time.
Expand All @@ -279,15 +275,13 @@ class Thing(
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)

monitoring_frequencies: Mapped[List["MonitoringFrequencyHistory"]] = relationship(
"MonitoringFrequencyHistory",
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)

# One-To-Many: A Thing can be associated with many AquiferSystems via the ThingAquiferAssociation join table.
Expand All @@ -296,7 +290,6 @@ class Thing(
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)

# Many-To-Many: A Thing can penetrate many GeologicFormations.
Expand All @@ -306,7 +299,6 @@ class Thing(
back_populates="thing",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)
)

Expand Down Expand Up @@ -348,9 +340,6 @@ class Thing(
# Full-text search vector
search_vector = Column(TSVectorType("name"))

# for temporary backwards compatibility
well_construction_notes = mapped_column(String(1000), nullable=True)

@property
def current_location(self):
"""
Expand Down
5 changes: 2 additions & 3 deletions db/thing_aquifer_association.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,17 @@ class ThingAquiferAssociation(Base, AutoBaseMixin, ReleaseMixin):
# --- Relationship Definitions ---
# Many-To-One: This association links to one Thing.
thing: Mapped["Thing"] = relationship(
"Thing", back_populates="aquifer_associations", lazy="joined"
"Thing", back_populates="aquifer_associations"
)

# Many-To-One: This association links to one AquiferSystem.
aquifer_system: Mapped["AquiferSystem"] = relationship(
"AquiferSystem", back_populates="thing_associations", lazy="joined"
"AquiferSystem", back_populates="thing_associations"
)
# One-To-Many: An association can have multiple aquifer types.
aquifer_types: Mapped[list["AquiferType"]] = relationship(
"AquiferType",
back_populates="thing_aquifer_association",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="joined",
)
4 changes: 2 additions & 2 deletions db/thing_geologic_formation_association.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ class ThingGeologicFormationAssociation(Base, AutoBaseMixin, ReleaseMixin):
# --- Relationship Definitions ---
# Many-To-One: This association links to one Thing.
thing: Mapped["Thing"] = relationship(
"Thing", back_populates="formation_associations", lazy="joined"
"Thing", back_populates="formation_associations"
)

# Many-To-One: This association links to one GeologicFormation.
geologic_formation: Mapped["GeologicFormation"] = relationship(
"GeologicFormation", back_populates="thing_associations", lazy="joined"
"GeologicFormation", back_populates="thing_associations"
)
12 changes: 10 additions & 2 deletions schemas/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

from core.enums import Role, ContactType, PhoneType, EmailType, AddressType
from schemas import BaseResponseModel, BaseCreateModel, BaseUpdateModel
from schemas.thing import ThingResponse


# -------- VALIDATORS ----------
Expand Down Expand Up @@ -199,6 +198,15 @@ class AddressResponse(BaseItemResponse):
address_type: AddressType


class ThingResponseForContact(BaseResponseModel):
"""
Response schema for thing details related to a contact. All that is needed
are the id and name
"""

name: str


class ContactResponse(BaseResponseModel):
"""
Response schema for contact details.
Expand All @@ -212,7 +220,7 @@ class ContactResponse(BaseResponseModel):
emails: List[EmailResponse] = []
phones: List[PhoneResponse] = []
addresses: List[AddressResponse] = []
things: List[ThingResponse] = [] # List of related things
things: List[ThingResponseForContact] = []

@field_validator("incomplete_nma_phones", mode="before")
def make_incomplete_nma_phone_str(cls, v: list) -> list:
Expand Down
54 changes: 45 additions & 9 deletions services/thing_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
from datetime import datetime
from zoneinfo import ZoneInfo

from fastapi import Request
from fastapi import Request, HTTPException
from fastapi_pagination.ext.sqlalchemy import paginate
from pydantic import BaseModel
from shapely import wkb
from shapely.geometry import mapping
from sqlalchemy import select, func
from sqlalchemy.orm import Session, aliased
from sqlalchemy.orm import Session, aliased, selectinload
from starlette.status import HTTP_404_NOT_FOUND, HTTP_409_CONFLICT

from db import (
Expand All @@ -33,9 +33,11 @@
WellScreen,
WellPurpose,
WellCasingMaterial,
ThingAquiferAssociation,
GroupThingAssociation,
MeasuringPointHistory,
)
from db.group import GroupThingAssociation
from db.measuring_point_history import MeasuringPointHistory

from services.audit_helper import audit_add
from services.crud_helper import model_patcher
from services.exceptions_helper import PydanticStyleException
Expand All @@ -47,6 +49,22 @@
"well_casing_materials": (WellCasingMaterial, "material"),
}

WELL_LOADER_OPTIONS = [
selectinload(Thing.location_associations).selectinload(
LocationThingAssociation.location
),
selectinload(Thing.well_purposes),
selectinload(Thing.well_casing_materials),
selectinload(Thing.links),
selectinload(Thing.measuring_points),
selectinload(Thing.monitoring_frequencies),
selectinload(Thing.aquifer_associations).selectinload(
ThingAquiferAssociation.aquifer_system
),
]

WELL_THING_TYPE = "water well"


def wkb_to_geojson(wkb_element):
if wkb_element is None:
Expand Down Expand Up @@ -74,6 +92,12 @@ def get_db_things(
if thing_type:
sql = sql.where(Thing.thing_type == thing_type)

if thing_type == WELL_THING_TYPE:
sql = sql.options(*WELL_LOADER_OPTIONS)
else:
# add all eager loads for generic thing query until/unless GET /thing is deprecated
sql = sql.options(*WELL_LOADER_OPTIONS)

if name:
sql = sql.where(Thing.name == name)

Expand Down Expand Up @@ -118,8 +142,7 @@ def get_thing_type_from_request(request: Request) -> str:
return thing_type


def verify_thing_type_correspondence(thing: Thing, request: Request):
thing_type = get_thing_type_from_request(request)
def verify_thing_type_correspondence(thing: Thing, thing_type: str):
if thing.thing_type != thing_type:
raise PydanticStyleException(
status_code=HTTP_404_NOT_FOUND,
Expand All @@ -135,9 +158,21 @@ def verify_thing_type_correspondence(thing: Thing, request: Request):


def get_thing_of_a_thing_type_by_id(session: Session, request: Request, thing_id: int):
thing = simple_get_by_id(session, Thing, thing_id)
thing_type = get_thing_type_from_request(request)
sql = select(Thing).where(Thing.id == thing_id)

if thing_type == WELL_THING_TYPE:
sql = sql.options(*WELL_LOADER_OPTIONS)

verify_thing_type_correspondence(thing, request)
thing = session.execute(sql).scalar_one_or_none()

if not thing:
raise HTTPException(
status_code=HTTP_404_NOT_FOUND,
detail=f"Thing with ID {thing_id} not found.",
)

verify_thing_type_correspondence(thing, thing_type)

return thing

Expand Down Expand Up @@ -261,7 +296,8 @@ def patch_thing(
):
thing = simple_get_by_id(session, Thing, thing_id)

verify_thing_type_correspondence(thing, request)
thing_type = get_thing_type_from_request(request)
verify_thing_type_correspondence(thing, thing_type)

thing = model_patcher(session, Thing, thing_id, payload, user)
return thing
Expand Down
Loading