diff --git a/api/observation.py b/api/observation.py index f4fa65ec4..51b100ebe 100644 --- a/api/observation.py +++ b/api/observation.py @@ -30,12 +30,9 @@ GroundwaterLevelObservationResponse, CreateWaterChemistryObservation, WaterChemistryObservationResponse, - # CreateGeothermalObservation, - # GeothermalObservationResponse, ObservationResponse, UpdateGroundwaterLevelObservation, UpdateWaterChemistryObservation, - # UpdateGeothermalObservation, ) from services.crud_helper import model_deleter, model_adder from services.query_helper import simple_get_by_id @@ -81,19 +78,6 @@ async def add_water_chemistry_observation( return model_adder(session, Observation, obs_data, user=user) -# @router.post("/geothermal", status_code=HTTP_201_CREATED) -# async def add_geothermal_observation( -# obs_data: CreateGeothermalObservation, -# session: session_dependency, -# user: admin_dependency, -# ) -> GeothermalObservationResponse: -# """ -# Add a new geothermal observation to the database. -# This endpoint is currently a placeholder and does not implement any functionality. -# """ -# return model_adder(session, Observation, obs_data, user=user) - - # PATCH ======================================================================== @@ -125,20 +109,6 @@ async def update_water_chemistry_observation( return observation_model_patcher(session, request, observation_id, obs_data, user) -# @router.patch("/geothermal/{observation_id}", status_code=HTTP_200_OK) -# async def update_geothermal_observation( -# observation_id: int, -# obs_data: UpdateGeothermalObservation, -# session: session_dependency, -# user: admin_dependency, -# request: Request, -# ) -> GeothermalObservationResponse: -# """ -# Update an existing geothermal observation in the database. -# """ -# return observation_model_patcher(session, request, observation_id, obs_data, user) - - # ============= Get ============================================== @@ -237,49 +207,6 @@ async def get_water_chemistry_observation_by_id( ) -# @router.get("/geothermal", summary="Get geothermal observations") -# async def get_geothermal_observations( -# request: Request, -# session: session_dependency, -# user: viewer_dependency, -# thing_id: int | None = None, -# sensor_id: int | None = None, -# sample_id: int | None = None, -# start_time: datetime | None = None, -# end_time: datetime | None = None, -# sort: str | None = None, -# order: str | None = None, -# filter_: str = Query(alias="filter", default=None), -# ) -> CustomPage[GeothermalObservationResponse]: -# """ -# Retrieve all geothermal observations from the database. -# """ -# return get_observations( -# request=request, -# session=session, -# thing_id=thing_id, -# sensor_id=sensor_id, -# sample_id=sample_id, -# start_time=start_time, -# end_time=end_time, -# sort=sort, -# order=order, -# filter_=filter_, -# ) - - -# @router.get("/geothermal/{observation_id}", summary="Get geothermal observation by ID") -# async def get_geothermal_observation_by_id( -# session: session_dependency, -# request: Request, -# user: amp_viewer_dependency, -# observation_id: int, -# ) -> GeothermalObservationResponse: -# return get_observation_of_an_activity_type_by_id( -# session=session, request=request, observation_id=observation_id -# ) - - @router.get("", summary="Get all observations") async def get_all_observations( request: Request, diff --git a/core/lexicon.json b/core/lexicon.json index d78b1281c..17f27765e 100644 --- a/core/lexicon.json +++ b/core/lexicon.json @@ -20,7 +20,7 @@ {"name": "field_contact_role", "description": null}, {"name": "geochronology", "description": null}, {"name": "horizontal_datum", "description": null}, - {"name": "level_status", "description": null}, + {"name": "value_reason", "description": null}, {"name": "measurement_method", "description": null}, {"name": "monitoring_status", "description": null}, {"name": "observed_property", "description": null}, @@ -284,29 +284,30 @@ {"categories": ["thing_type"], "term": "perennial stream", "definition": "that has a continuous flow of water throughout the year, even during drier periods."}, {"categories": ["thing_type"], "term": "ephemeral stream", "definition": "a stream that flows only briefly during and after precipitation events"}, {"categories": ["thing_type"], "term": "meteorological station", "definition": "a station that measures the weather conditions at a particular location"}, - {"categories": ["level_status"], "term": "Water level affected by atmospheric pressure", "definition": "Water level affected by atmospheric pressure"}, - {"categories": ["level_status"], "term": "Water level was frozen (no level recorded).", "definition": "Water level was frozen (no level recorded)."}, - {"categories": ["level_status"], "term": "Site was dry", "definition": "Site was dry"}, - {"categories": ["level_status"], "term": "Site was flowing recently.", "definition": "Site was flowing recently."}, - {"categories": ["level_status"], "term": "Site was flowing. Water level or head couldn't be measured w/out additional equipment.", "definition": "Site was flowing. Water level or head couldn't be measured w/out additional equipment."}, - {"categories": ["level_status"], "term": "Nearby site that taps the same aquifer was flowing.", "definition": "Nearby site that taps the same aquifer was flowing."}, - {"categories": ["level_status"], "term": "Nearby site that taps the same aquifer had been flowing recently.", "definition": "Nearby site that taps the same aquifer had been flowing recently."}, - {"categories": ["level_status"], "term": "Recharge water was being injected into the aquifer at this site.", "definition": "Recharge water was being injected into the aquifer at this site."}, - {"categories": ["level_status"], "term": "Recharge water was being injected into nearby site that taps the same aquifer.", "definition": "Recharge water was being injected into nearby site that taps the same aquifer."}, - {"categories": ["level_status"], "term": "Water was cascading down the inside of the well.", "definition": "Water was cascading down the inside of the well."}, - {"categories": ["level_status"], "term": "Water level was affected by brackish or saline water.", "definition": "Water level was affected by brackish or saline water."}, - {"categories": ["level_status"], "term": "Well was not in hydraulic contact w/formation (from source other than defined in USGS C714 or C93).", "definition": "Well was not in hydraulic contact w/formation (from source other than defined in USGS C714 or C93)."}, - {"categories": ["level_status"], "term": "Measurement was discontinued (no level recorded).", "definition": "Measurement was discontinued (no level recorded)."}, - {"categories": ["level_status"], "term": "Obstruction was encountered in the well (no level recorded)", "definition": "Obstruction was encountered in the well (no level recorded)"}, - {"categories": ["level_status"], "term": "Site was being pumped", "definition": "Site was being pumped"}, - {"categories": ["level_status"], "term": "Site was pumped recently", "definition": "Site was pumped recently"}, - {"categories": ["level_status"], "term": "Nearby site that taps the same aquifer was being pumped", "definition": "Nearby site that taps the same aquifer was being pumped"}, - {"categories": ["level_status"], "term": "Nearby site that taps the same aquifer was pumped recently", "definition": "Nearby site that taps the same aquifer was pumped recently"}, - {"categories": ["level_status"], "term": "Foreign substance present on the water surface", "definition": "Foreign substance present on the water surface"}, - {"categories": ["level_status"], "term": "Well was destroyed (no subsequent water levels should be recorded)", "definition": "Well was destroyed (no subsequent water levels should be recorded)"}, - {"categories": ["level_status"], "term": "Water level affected by stage in nearby surface-water site", "definition": "Water level affected by stage in nearby surface-water site"}, - {"categories": ["level_status"], "term": "Other conditions exist that would affect the level (remarks)", "definition": "Other conditions exist that would affect the level (remarks)"}, - {"categories": ["level_status"], "term": "Water level not affected by status", "definition": "Water level not affected by status"}, + {"categories": ["value_reason"], "term": "Water level affected by atmospheric pressure", "definition": "Water level affected by atmospheric pressure"}, + {"categories": ["value_reason"], "term": "Water level was frozen (no level recorded).", "definition": "Water level was frozen (no level recorded)."}, + {"categories": ["value_reason"], "term": "Site was dry", "definition": "Site was dry"}, + {"categories": ["value_reason"], "term": "Site was flowing recently.", "definition": "Site was flowing recently."}, + {"categories": ["value_reason"], "term": "Site was flowing. Water level or head couldn't be measured w/out additional equipment.", "definition": "Site was flowing. Water level or head couldn't be measured w/out additional equipment."}, + {"categories": ["value_reason"], "term": "Nearby site that taps the same aquifer was flowing.", "definition": "Nearby site that taps the same aquifer was flowing."}, + {"categories": ["value_reason"], "term": "Nearby site that taps the same aquifer had been flowing recently.", "definition": "Nearby site that taps the same aquifer had been flowing recently."}, + {"categories": ["value_reason"], "term": "Recharge water was being injected into the aquifer at this site.", "definition": "Recharge water was being injected into the aquifer at this site."}, + {"categories": ["value_reason"], "term": "Recharge water was being injected into nearby site that taps the same aquifer.", "definition": "Recharge water was being injected into nearby site that taps the same aquifer."}, + {"categories": ["value_reason"], "term": "Water was cascading down the inside of the well.", "definition": "Water was cascading down the inside of the well."}, + {"categories": ["value_reason"], "term": "Water level was affected by brackish or saline water.", "definition": "Water level was affected by brackish or saline water."}, + {"categories": ["value_reason"], "term": "Well was not in hydraulic contact w/formation (from source other than defined in USGS C714 or C93).", "definition": "Well was not in hydraulic contact w/formation (from source other than defined in USGS C714 or C93)."}, + {"categories": ["value_reason"], "term": "Measurement was discontinued (no level recorded).", "definition": "Measurement was discontinued (no level recorded)."}, + {"categories": ["value_reason"], "term": "Obstruction was encountered in the well (no level recorded)", "definition": "Obstruction was encountered in the well (no level recorded)"}, + {"categories": ["value_reason"], "term": "Site was being pumped", "definition": "Site was being pumped"}, + {"categories": ["value_reason"], "term": "Site was pumped recently", "definition": "Site was pumped recently"}, + {"categories": ["value_reason"], "term": "Nearby site that taps the same aquifer was being pumped", "definition": "Nearby site that taps the same aquifer was being pumped"}, + {"categories": ["value_reason"], "term": "Nearby site that taps the same aquifer was pumped recently", "definition": "Nearby site that taps the same aquifer was pumped recently"}, + {"categories": ["value_reason"], "term": "Foreign substance present on the water surface", "definition": "Foreign substance present on the water surface"}, + {"categories": ["value_reason"], "term": "Well was destroyed (no subsequent water levels should be recorded)", "definition": "Well was destroyed (no subsequent water levels should be recorded)"}, + {"categories": ["value_reason"], "term": "Water level affected by stage in nearby surface-water site", "definition": "Water level affected by stage in nearby surface-water site"}, + {"categories": ["value_reason"], "term": "Other conditions exist that would affect the level (remarks)", "definition": "Other conditions exist that would affect the level (remarks)"}, + {"categories": ["value_reason"], "term": "Water level not affected by status", "definition": "Water level not affected by status"}, + {"categories": ["value_reason"], "term": "Observed value not affected", "definition": "Observed value not affected"}, {"categories": ["status"], "term": "Abandoned", "definition": "Abandoned"}, {"categories": ["status"], "term": "Active, pumping well", "definition": "Active, pumping well"}, {"categories": ["status"], "term": "Destroyed, exists but not usable", "definition": "Destroyed, exists but not usable"}, diff --git a/db/contact.py b/db/contact.py index 83a87cc39..3f4995f67 100644 --- a/db/contact.py +++ b/db/contact.py @@ -21,12 +21,12 @@ from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term + if TYPE_CHECKING: + from db.field import FieldEventParticipant, FieldEvent from db.thing import Thing - from db.field import FieldEvent - from db.field import FieldEventParticipants + from db.publication import Author, AuthorContactAssociation from db.permission import Permission - from db.publication import AuthorContactAssociation, Author class ThingContactAssociation(Base, AutoBaseMixin): @@ -42,7 +42,7 @@ class ThingContactAssociation(Base, AutoBaseMixin): ) thing: Mapped["Thing"] = relationship( "Thing", back_populates="contact_associations" - ) # noqa: F821 + ) class Contact(Base, AutoBaseMixin, ReleaseMixin): @@ -85,8 +85,8 @@ class Contact(Base, AutoBaseMixin, ReleaseMixin): cascade="all, delete-orphan", ) # One-To-Many: A Contact can participate in many Field Events. - field_event_participants: Mapped[list["FieldEventParticipants"]] = relationship( - "FieldEventParticipants", + field_event_participants: Mapped[list["FieldEventParticipant"]] = relationship( + "FieldEventParticipant", back_populates="contact", cascade="all, delete-orphan", passive_deletes=True, diff --git a/db/field.py b/db/field.py index 2aaef2726..551859dcc 100644 --- a/db/field.py +++ b/db/field.py @@ -2,12 +2,17 @@ from sqlalchemy import DateTime, ForeignKey from sqlalchemy.orm import mapped_column, relationship, Mapped from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy +from typing import TYPE_CHECKING from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term -from db.contact import Contact +if TYPE_CHECKING: + from db.contact import Contact + from db.sample import Sample + from db.thing import Thing -class FieldEventParticipants(Base, AutoBaseMixin, ReleaseMixin): + +class FieldEventParticipant(Base, AutoBaseMixin, ReleaseMixin): """ This association table is to create a many-to-many relationship between FieldEvent and Contact. These are participants in the field event. @@ -34,15 +39,15 @@ class FieldEventParticipants(Base, AutoBaseMixin, ReleaseMixin): field_event: Mapped["FieldEvent"] = relationship( "FieldEvent", back_populates="field_event_participants" ) - contact: Mapped["Contact"] = relationship( # noqa: F821 + contact: Mapped["Contact"] = relationship( "Contact", back_populates="field_event_participants" ) # map associated contacts to samples to restrict the people who could have # taken a sample to those present at the field event - samples: Mapped[list["Sample"]] = relationship( # noqa: F821 + samples: Mapped[list["Sample"]] = relationship( "Sample", - back_populates="field_event_contact", + back_populates="field_event_participant", passive_deletes=True, ) @@ -60,7 +65,7 @@ class FieldEvent(Base, AutoBaseMixin, ReleaseMixin): Its purpose is to store the "where and when" of the event. Information about who participated is managed in the - FieldEventContactAssociation table. Information about the "what" of the + FieldEventParticipant table. Information about the "what" of the event is managed in the FieldActivity and Sample tables. """ @@ -82,12 +87,12 @@ class FieldEvent(Base, AutoBaseMixin, ReleaseMixin): comment="Notes or comments about the field event.", ) # --- Relationships --- - thing: Mapped["Thing"] = relationship(back_populates="field_events") # noqa: F821 + thing: Mapped["Thing"] = relationship(back_populates="field_events") field_activities: Mapped[list["FieldActivity"]] = relationship( "FieldActivity", back_populates="field_event" ) - field_event_participants: Mapped[list["FieldEventParticipants"]] = relationship( - "FieldEventParticipants", + field_event_participants: Mapped[list["FieldEventParticipant"]] = relationship( + "FieldEventParticipant", back_populates="field_event", cascade="all, delete-orphan", passive_deletes=True, @@ -95,7 +100,7 @@ class FieldEvent(Base, AutoBaseMixin, ReleaseMixin): # --- Association Proxies --- # Proxy to directly access the Contact objects participating in this event. - contacts: AssociationProxy[list["Contact"]] = association_proxy( # noqa: F821 + contacts: AssociationProxy[list["Contact"]] = association_proxy( "field_event_participants", "contact" ) @@ -136,7 +141,7 @@ class FieldActivity(Base, AutoBaseMixin, ReleaseMixin): field_event: Mapped["FieldEvent"] = relationship( "FieldEvent", back_populates="field_activities" ) - samples: Mapped[list["Sample"]] = relationship( # noqa: F821 + samples: Mapped[list["Sample"]] = relationship( "Sample", back_populates="field_activity", cascade="all, delete-orphan", diff --git a/db/location.py b/db/location.py index 102590606..6ef7ce8f5 100644 --- a/db/location.py +++ b/db/location.py @@ -14,7 +14,7 @@ # limitations under the License. # =============================================================================== import datetime - +from typing import TYPE_CHECKING from geoalchemy2 import Geometry, WKBElement from geoalchemy2.shape import to_shape @@ -34,6 +34,9 @@ from db.base import Base, AutoBaseMixin, ReleaseMixin from db.lexicon import lexicon_term +if TYPE_CHECKING: + from db.thing import Thing + class Location(Base, AutoBaseMixin, ReleaseMixin): __versioned__ = {} @@ -67,7 +70,7 @@ class Location(Base, AutoBaseMixin, ReleaseMixin): ) # --- Proxy Definitions --- - things: AssociationProxy[list["Thing"]] = association_proxy( # noqa: F821 + things: AssociationProxy[list["Thing"]] = association_proxy( "thing_associations", "thing" ) @@ -97,9 +100,7 @@ class LocationThingAssociation(Base, AutoBaseMixin): # --- Relationship Definitions --- location: Mapped["Location"] = relationship(back_populates="thing_associations") - thing: Mapped["Thing"] = relationship( # noqa: F821 - back_populates="location_associations" - ) + thing: Mapped["Thing"] = relationship(back_populates="location_associations") # ============= EOF ============================================= diff --git a/db/observation.py b/db/observation.py index af737a7cf..588b77d0a 100644 --- a/db/observation.py +++ b/db/observation.py @@ -59,6 +59,11 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): nullable=True, ) unit: Mapped[str] = lexicon_term(nullable=False) + value_reason: Mapped[str] = lexicon_term( + nullable=False, + comment="The reason describes everything that can effect the observation the moment a sample/observation is attempted (e.g. obstruction, dry well, equipment failure); a null value must have an associated reason in the same record. Factors preventing the obtainment of the observation from the beginning of the field event to attempted sampling/observation (e.g. flat tire, locked gate, destroyed well) are not recorded here but in the notes field of the FieldEvent table; in this situation no sample/observation should be recorded.", + ) + notes: Mapped[str] = mapped_column(nullable=True) # groundwater measuring_point_height: Mapped[float] = mapped_column( @@ -67,25 +72,16 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): info={"unit": "ft"}, ) - level_status: Mapped[str] = lexicon_term(nullable=True) - - # geothermal - observation_depth: Mapped[float] = mapped_column( - nullable=True, - info={"unit": "feet"}, - doc="Depth of the geothermal observation in feet", - ) - # --- Relationships --- # Many-To-One: An Observation can be generated by one piece of Equipment. - sensor: Mapped["Sensor"] = relationship( # noqa: F821 + sensor: Mapped["Sensor"] = relationship( "Sensor", back_populates="observations", passive_deletes=True - ) # noqa: F821 + ) # Many-To-One: An Observation is derived from one Sample. - sample: Mapped["Sample"] = relationship( # noqa: F821 + sample: Mapped["Sample"] = relationship( "Sample", back_populates="observations", passive_deletes=True - ) # noqa: F821 + ) # Many-To-One: An Observation can be generated using one AnalysisMethod. analysis_method: Mapped["AnalysisMethod"] = relationship( diff --git a/db/publication.py b/db/publication.py index 2eec8213a..9cfda90f0 100644 --- a/db/publication.py +++ b/db/publication.py @@ -14,6 +14,7 @@ # limitations under the License. # =============================================================================== from sqlalchemy_utils import TSVectorType +from typing import TYPE_CHECKING from db import lexicon_term from db.base import AutoBaseMixin, Base, AuditMixin @@ -22,6 +23,10 @@ from sqlalchemy.ext.associationproxy import association_proxy, AssociationProxy +if TYPE_CHECKING: + from db.contact import Contact + + class Publication(Base, AutoBaseMixin): title = Column(Text, nullable=False) @@ -76,7 +81,7 @@ class Author(Base, AutoBaseMixin): back_populates="author", cascade="all, delete-orphan", ) - contacts: AssociationProxy[list["Contact"]] = association_proxy( # noqa: F821 + contacts: AssociationProxy[list["Contact"]] = association_proxy( "author_associations", "contact" ) diff --git a/db/sample.py b/db/sample.py index 2128d5635..04e647c62 100644 --- a/db/sample.py +++ b/db/sample.py @@ -24,7 +24,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from db.field import FieldEvent, FieldActivity, FieldEventParticipants + from db.field import FieldEvent, FieldActivity, FieldEventParticipant from db.thing import Thing from db.contact import Contact from db.observation import Observation @@ -50,8 +50,8 @@ class Sample(Base, AutoBaseMixin, ReleaseMixin): ForeignKey("field_activity.id"), nullable=False ) - field_event_contact_id: Mapped[str] = mapped_column( - ForeignKey("field_event_participants.id"), nullable=True + field_event_participant_id: Mapped[str] = mapped_column( + ForeignKey("field_event_participant.id"), nullable=True ) # --- Columns --- @@ -92,7 +92,7 @@ class Sample(Base, AutoBaseMixin, ReleaseMixin): # --- Relationship Definitions --- field_activity: Mapped["FieldActivity"] = relationship(back_populates="samples") - field_event_contact: Mapped["FieldEventParticipants"] = relationship( + field_event_participant: Mapped["FieldEventParticipant"] = relationship( back_populates="samples" ) @@ -104,7 +104,7 @@ class Sample(Base, AutoBaseMixin, ReleaseMixin): "field_activity", "field_event.thing" ) contact: AssociationProxy["Contact"] = association_proxy( - "field_event_contact", "contact" + "field_event_participant", "contact" ) observations: Mapped[list["Observation"]] = relationship( "Observation", diff --git a/db/sensor.py b/db/sensor.py index bc3f243fb..5face8a96 100644 --- a/db/sensor.py +++ b/db/sensor.py @@ -50,7 +50,7 @@ class Sensor(Base, AutoBaseMixin, ReleaseMixin): # --- Relationships --- # One-To-Many: A piece of Equipment can generate many Observations. - observations: Mapped[List["Observation"]] = relationship( # noqa: F821 + observations: Mapped[List["Observation"]] = relationship( "Observation", back_populates="sensor", ) diff --git a/schemas/observation.py b/schemas/observation.py index 1e052fec3..819547095 100644 --- a/schemas/observation.py +++ b/schemas/observation.py @@ -64,21 +64,17 @@ class CreateBaseObservation(BaseCreateModel, ValidateObservation): release_status: str value: float | None unit: str | None + value_reason: str class CreateGroundwaterLevelObservation(CreateBaseObservation): measuring_point_height: float - level_status: str class CreateWaterChemistryObservation(CreateBaseObservation): pass -# class CreateGeothermalObservation(CreateBaseObservation): -# observation_depth: float - - # -------- UPDATE ------------ @@ -90,21 +86,17 @@ class UpdateBaseObservation(BaseUpdateModel, ValidateObservation): release_status: str | None = None value: float | None | None = None unit: str | None = None + value_reason: str | None = None class UpdateGroundwaterLevelObservation(UpdateBaseObservation): measuring_point_height: float | None = None - level_status: str | None = None class UpdateWaterChemistryObservation(UpdateBaseObservation): pass -# class UpdateGeothermalObservation(UpdateBaseObservation): -# observation_depth: float | None = None - - # -------- RESPONSE ---------- class BaseObservationResponse(BaseResponseModel): sample_id: int @@ -114,12 +106,12 @@ class BaseObservationResponse(BaseResponseModel): release_status: str value: float | None unit: str + value_reason: str class GroundwaterLevelObservationResponse(BaseObservationResponse): depth_to_water_bgs: float | None measuring_point_height: float | None - level_status: str | None @model_validator(mode="before") def calculate_depth_to_water_bgs(self: Self) -> Self: @@ -136,10 +128,6 @@ class WaterChemistryObservationResponse(BaseObservationResponse): pass -# class GeothermalObservationResponse(BaseObservationResponse): -# observation_depth: float | None - - class ObservationResponse( GroundwaterLevelObservationResponse, WaterChemistryObservationResponse ): diff --git a/schemas/sample.py b/schemas/sample.py index 44a29aef9..43c364beb 100644 --- a/schemas/sample.py +++ b/schemas/sample.py @@ -84,7 +84,7 @@ def convert_sample_date_to_utc(sample_date: AwareDatetime) -> AwareDatetime: # -------- CREATE ---------- class CreateSample(BaseCreateModel, ValidateSample): field_activity_id: int - field_event_contact_id: int + field_event_participant_id: int sample_date: Annotated[AwareDatetime, PastDatetime()] sample_name: str sample_matrix: str @@ -98,7 +98,7 @@ class CreateSample(BaseCreateModel, ValidateSample): # -------- UPDATE ---------- class UpdateSample(BaseUpdateModel, ValidateSample): field_activity_id: int | None = None # TODO: should this be editable? - field_event_contact_id: int | None = None + field_event_participant_id: int | None = None sample_date: Annotated[AwareDatetime, PastDatetime()] | None = None sample_name: str | None = None sample_matrix: str | None = None @@ -124,8 +124,6 @@ class SampleResponse(BaseResponseModel): field_event: FieldEventResponse field_activity: FieldActivityResponse contact: ContactResponse - # field_activity_id: int - # field_event_contact_id: int sample_date: Annotated[AwareDatetime, PastDatetime()] sample_name: str sample_matrix: str diff --git a/services/observation_helper.py b/services/observation_helper.py index 7d935c346..650c66a28 100644 --- a/services/observation_helper.py +++ b/services/observation_helper.py @@ -12,7 +12,6 @@ from schemas.observation import ( ObservationResponse, WaterChemistryObservationResponse, - # GeothermalObservationResponse, GroundwaterLevelObservationResponse, ) from services.exceptions_helper import PydanticStyleException @@ -47,7 +46,6 @@ def get_observations( ) -> ( List[ObservationResponse] | List[WaterChemistryObservationResponse] - # | List[GeothermalObservationResponse] | List[GroundwaterLevelObservationResponse] ): """ diff --git a/services/sample_helper.py b/services/sample_helper.py index f98bf6e07..e8fecab4d 100644 --- a/services/sample_helper.py +++ b/services/sample_helper.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session, joinedload from fastapi_pagination.ext.sqlalchemy import paginate -from db import FieldEvent, FieldActivity, FieldEventParticipants, Sample +from db import FieldEvent, FieldActivity, FieldEventParticipant, Sample from services.query_helper import order_sort_filter @@ -17,8 +17,8 @@ def get_db_samples( joinedload(Sample.field_activity) .joinedload(FieldActivity.field_event) .joinedload(FieldEvent.thing), - joinedload(Sample.field_event_contact).joinedload( - FieldEventParticipants.contact + joinedload(Sample.field_event_participant).joinedload( + FieldEventParticipant.contact ), # Eagerly load related Contact ) diff --git a/tests/conftest.py b/tests/conftest.py index ed00d4277..062cf5e9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -496,16 +496,16 @@ def field_event(water_well_thing): @pytest.fixture(scope="session") -def field_event_contact(field_event, contact): +def field_event_participant(field_event, contact): with session_ctx() as session: - field_event_contact = FieldEventParticipants( + field_event_participant = FieldEventParticipant( field_event_id=field_event.id, contact_id=contact.id, field_contact_role="Lead", ) - session.add(field_event_contact) + session.add(field_event_participant) session.commit() - yield field_event_contact + yield field_event_participant @pytest.fixture(scope="session") @@ -537,11 +537,11 @@ def water_chemistry_field_activity(field_event): @pytest.fixture(scope="session") -def groundwater_level_sample(groundwater_level_field_activity, field_event_contact): +def groundwater_level_sample(groundwater_level_field_activity, field_event_participant): with session_ctx() as session: sample = Sample( field_activity_id=groundwater_level_field_activity.id, - field_event_contact_id=field_event_contact.id, + field_event_participant_id=field_event_participant.id, sample_date="2025-01-01T12:00:00Z", sample_name="groundwater level sample name", sample_matrix="water", @@ -558,11 +558,11 @@ def groundwater_level_sample(groundwater_level_field_activity, field_event_conta @pytest.fixture(scope="session") -def water_chemistry_sample(water_chemistry_field_activity, field_event_contact): +def water_chemistry_sample(water_chemistry_field_activity, field_event_participant): with session_ctx() as session: sample = Sample( field_activity_id=water_chemistry_field_activity.id, - field_event_contact_id=field_event_contact.id, + field_event_participant_id=field_event_participant.id, sample_date="2025-01-01T13:00:00Z", sample_name="water chemistry sample name", sample_matrix="water", @@ -579,11 +579,11 @@ def water_chemistry_sample(water_chemistry_field_activity, field_event_contact): @pytest.fixture(scope="function") -def sample_to_delete(water_chemistry_field_activity, field_event_contact): +def sample_to_delete(water_chemistry_field_activity, field_event_participant): with session_ctx() as session: sample = Sample( field_activity_id=water_chemistry_field_activity.id, - field_event_contact_id=field_event_contact.id, + field_event_participant_id=field_event_participant.id, sample_date="2025-01-01T13:00:00Z", sample_name="sample to delete", sample_matrix="water", @@ -613,7 +613,7 @@ def groundwater_level_observation(sensor, groundwater_level_sample): value=10.0, unit="ft", measuring_point_height=5.0, - level_status="Water level not affected by status", + value_reason="Water level not affected by status", ) session.add(observation) session.commit() @@ -631,24 +631,7 @@ def water_chemistry_observation(sensor, water_chemistry_sample): release_status="draft", value=4.0, unit="dimensionless", - ) - session.add(observation) - session.commit() - yield observation - - -@pytest.fixture(scope="session") -def geothermal_observation(sensor, geothermal_sample): - with session_ctx() as session: - observation = Observation( - observation_datetime="2025-01-01T00:02:00Z", - sample_id=geothermal_sample.id, - sensor_id=sensor.id, - observed_property="temperature", - release_status="draft", - value=20.0, - unit="deg C", - observation_depth=200.0, + value_reason="Observed value not affected", ) session.add(observation) session.commit() @@ -666,6 +649,7 @@ def observation_to_delete(water_chemistry_sample, sensor): release_status="draft", value=4.0, unit="dimensionless", + value_reason="Observed value not affected", ) session.add(observation) session.commit() diff --git a/tests/test_observation.py b/tests/test_observation.py index ec1c73060..ede2c90ca 100644 --- a/tests/test_observation.py +++ b/tests/test_observation.py @@ -55,6 +55,7 @@ def test_add_water_chemistry_observation(water_chemistry_sample, sensor): "sample_id": water_chemistry_sample.id, "sensor_id": sensor.id, "observed_property": "pH", + "value_reason": "Observed value not affected", } response = client.post("/observation/water-chemistry", json=payload) data = response.json() @@ -69,6 +70,7 @@ def test_add_water_chemistry_observation(water_chemistry_sample, sensor): assert data["sample_id"] == payload["sample_id"] assert data["sensor_id"] == payload["sensor_id"] assert data["observed_property"] == payload["observed_property"] + assert data["value_reason"] == payload["value_reason"] cleanup_post_test(Observation, data["id"]) @@ -81,7 +83,7 @@ def test_add_groundwater_level_observation(groundwater_level_sample, sensor): "measuring_point_height": 53, "sample_id": groundwater_level_sample.id, "sensor_id": sensor.id, - "level_status": "Water level not affected by status", + "value_reason": "Water level not affected by status", "observed_property": "groundwater level", "unit": "ft", } @@ -96,7 +98,7 @@ def test_add_groundwater_level_observation(groundwater_level_sample, sensor): assert data["value"] == payload["value"] assert data["measuring_point_height"] == payload["measuring_point_height"] assert data["sensor_id"] == payload["sensor_id"] - assert data["level_status"] == payload["level_status"] + assert data["value_reason"] == payload["value_reason"] assert data["observed_property"] == payload["observed_property"] assert ( data["depth_to_water_bgs"] @@ -106,35 +108,6 @@ def test_add_groundwater_level_observation(groundwater_level_sample, sensor): cleanup_post_test(Observation, data["id"]) -# def test_add_geothermal_observation(geothermal_sample, sensor): -# payload = { -# "observation_datetime": "2025-01-01T00:00:00Z", -# "release_status": "draft", -# "observation_depth": 100, -# "value": 25.5, -# "sample_id": geothermal_sample.id, -# "sensor_id": sensor.id, -# "observed_property": "temperature", -# "unit": "deg C", -# } -# response = client.post("/observation/geothermal", json=payload) -# data = response.json() -# assert response.status_code == 201 - -# assert "id" in data -# assert "created_at" in data -# assert data["observation_datetime"] == payload["observation_datetime"] -# assert data["release_status"] == payload["release_status"] -# assert data["observation_depth"] == payload["observation_depth"] -# assert data["value"] == payload["value"] -# assert data["sample_id"] == payload["sample_id"] -# assert data["sensor_id"] == payload["sensor_id"] -# assert data["observed_property"] == payload["observed_property"] -# assert data["unit"] == payload["unit"] - -# cleanup_post_test(Observation, data["id"]) - - # PATCH tests ================================================================== @@ -224,48 +197,6 @@ def test_patch_water_chemistry_observation_404_wrong_activity_type( ) -# def test_patch_geothermal_observation(geothermal_observation): -# payload = {"observation_depth": 4, "release_status": "private"} -# response = client.patch( -# f"/observation/geothermal/{geothermal_observation.id}", json=payload -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["observation_depth"] == payload["observation_depth"] -# assert data["release_status"] == payload["release_status"] - -# cleanup_patch_test(Observation, payload, geothermal_observation) - - -# def test_patch_geothermal_observation_404_not_found(geothermal_observation): -# bad_id = 999999 -# payload = {"observation_depth": 8} -# response = client.patch(f"/observation/geothermal/{bad_id}", json=payload) -# assert response.status_code == 404 -# data = response.json() -# assert data["detail"] == f"Observation with ID {bad_id} not found." - - -# def test_patch_geothermal_observation_404_wrong_activity_type( -# groundwater_level_observation, water_chemistry_observation -# ): -# for obs in groundwater_level_observation, water_chemistry_observation: -# payload = {"value": 8} -# response = client.patch(f"/observation/geothermal/{obs.id}", json=payload) -# assert response.status_code == 404 -# data = response.json() - -# if obs.observed_property == "groundwater level": -# activity_type = "groundwater level" -# else: -# activity_type = "water chemistry" - -# assert ( -# data["detail"][0]["msg"] -# == f"Observation with ID {obs.id} is not a geothermal observation. It is a {activity_type} observation." -# ) - - # ============= Get tests ================= @@ -344,7 +275,7 @@ def test_get_groundwater_level_observations(groundwater_level_observation): == groundwater_level_observation.release_status ) assert ( - data["items"][0]["level_status"] == groundwater_level_observation.level_status + data["items"][0]["value_reason"] == groundwater_level_observation.value_reason ) assert data["items"][0]["value"] == groundwater_level_observation.value assert data["items"][0]["unit"] == groundwater_level_observation.unit @@ -358,7 +289,7 @@ def test_get_groundwater_level_observations(groundwater_level_observation): == groundwater_level_observation.measuring_point_height ) assert ( - data["items"][0]["level_status"] == groundwater_level_observation.level_status + data["items"][0]["value_reason"] == groundwater_level_observation.value_reason ) @@ -384,7 +315,7 @@ def test_get_groundwater_level_observation_by_id(groundwater_level_observation): == groundwater_level_observation.observed_property[colon_index + 1 :] ) assert data["release_status"] == groundwater_level_observation.release_status - assert data["level_status"] == groundwater_level_observation.level_status + assert data["value_reason"] == groundwater_level_observation.value_reason assert data["value"] == groundwater_level_observation.value assert data["unit"] == groundwater_level_observation.unit assert ( @@ -396,7 +327,7 @@ def test_get_groundwater_level_observation_by_id(groundwater_level_observation): data["measuring_point_height"] == groundwater_level_observation.measuring_point_height ) - assert data["level_status"] == groundwater_level_observation.level_status + assert data["value_reason"] == groundwater_level_observation.value_reason def test_get_groundwater_level_observation_by_id_404_not_found( @@ -592,87 +523,6 @@ def test_get_water_chemistry_observation_by_id_404_wrong_activity_type( assert data["detail"][0]["loc"] == ["path", "observation_id"] -# def test_get_geothermal_observations(geothermal_observation): -# response = client.get("/observation/geothermal") -# assert response.status_code == 200 -# data = response.json() -# assert data["total"] == 1 -# assert data["items"][0]["id"] == geothermal_observation.id -# assert data["items"][0][ -# "created_at" -# ] == geothermal_observation.created_at.isoformat().replace("+00:00", "Z") -# assert data["items"][0]["release_status"] == geothermal_observation.release_status -# assert data["items"][0]["sample_id"] == geothermal_observation.sample_id -# assert data["items"][0]["sensor_id"] == geothermal_observation.sensor_id -# assert ( -# data["items"][0]["observation_datetime"] -# == geothermal_observation.observation_datetime -# ) -# colon_index = geothermal_observation.observed_property.find(":") -# assert ( -# data["items"][0]["observed_property"] -# == geothermal_observation.observed_property[colon_index + 1 :] -# ) -# assert data["items"][0]["value"] == geothermal_observation.value -# assert data["items"][0]["unit"] == geothermal_observation.unit -# assert ( -# data["items"][0]["observation_depth"] -# == geothermal_observation.observation_depth -# ) - - -# def test_get_geothermal_observation_by_id(geothermal_observation): -# response = client.get(f"/observation/geothermal/{geothermal_observation.id}") -# assert response.status_code == 200 -# data = response.json() -# assert data["id"] == geothermal_observation.id -# assert data["created_at"] == geothermal_observation.created_at.isoformat().replace( -# "+00:00", "Z" -# ) -# assert data["release_status"] == geothermal_observation.release_status -# assert data["sample_id"] == geothermal_observation.sample_id -# assert data["sensor_id"] == geothermal_observation.sensor_id -# assert data["observation_datetime"] == geothermal_observation.observation_datetime -# colon_index = geothermal_observation.observed_property.find(":") -# assert ( -# data["observed_property"] -# == geothermal_observation.observed_property[colon_index + 1 :] -# ) -# assert data["value"] == geothermal_observation.value -# assert data["unit"] == geothermal_observation.unit -# assert data["observation_depth"] == geothermal_observation.observation_depth - - -# def test_get_geothermal_observation_by_id_404_not_found(geothermal_observation): -# bad_id = 99999 -# response = client.get(f"/observation/geothermal/{bad_id}") -# assert response.status_code == 404 -# data = response.json() -# assert data["detail"] == f"Observation with ID {bad_id} not found." - - -# def test_get_geothermal_observation_by_id_404_wrong_activity_type( -# water_chemistry_observation, groundwater_level_observation -# ): -# for obs in water_chemistry_observation, groundwater_level_observation: -# response = client.get(f"/observation/geothermal/{obs.id}") -# assert response.status_code == 404 -# data = response.json() - -# if obs.observed_property == "groundwater level": -# actual_activity_type = "groundwater level" -# else: -# actual_activity_type = "water chemistry" - -# assert ( -# data["detail"][0]["msg"] -# == f"Observation with ID {obs.id} is not a geothermal observation. It is a {actual_activity_type} observation." -# ) -# assert data["detail"][0]["type"] == "value_error" -# assert data["detail"][0]["input"] == {"observation_id": obs.id} -# assert data["detail"][0]["loc"] == ["path", "observation_id"] - - # JB's comment: I don't think that geographic filters are necessary for # observations. I think that they should only be applicable to finding Things # and locations. Then the user can proceed from there to find observations. diff --git a/tests/test_sample.py b/tests/test_sample.py index 4f5a301ee..c7fb83ee2 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -54,14 +54,14 @@ def test_validate_sample_top_and_bottom(): # ============= Post tests for samples ============================================= def test_add_sample( - groundwater_level_field_activity, water_well_thing, contact, field_event_contact + groundwater_level_field_activity, water_well_thing, contact, field_event_participant ): """ Test adding a sample. """ payload = { "field_activity_id": groundwater_level_field_activity.id, - "field_event_contact_id": field_event_contact.id, + "field_event_participant_id": field_event_participant.id, "sample_date": "2025-01-01T14:00:00Z", "sample_name": "second groundwater level field activity name", "sample_matrix": "water", @@ -102,14 +102,14 @@ def test_add_sample( def test_409_add_sample_invalid_sample_name( groundwater_level_field_activity, groundwater_level_sample, - field_event_contact, + field_event_participant, ): """ Test that a 409 error is raised if a duplicate sample_name is in the payload """ payload = { "field_activity_id": groundwater_level_field_activity.id, - "field_event_contact_id": field_event_contact.id, + "field_event_participant_id": field_event_participant.id, "sample_date": "2025-01-01T14:00:00Z", "sample_name": groundwater_level_sample.sample_name, "sample_matrix": "water", @@ -139,14 +139,14 @@ def test_409_add_sample_invalid_sample_name( def test_409_add_sample_invalid_field_activity_id( groundwater_level_field_activity, groundwater_level_sample, - field_event_contact, + field_event_participant, ): """ Test adding a sample with an invalid field_activity_id. """ payload = { "field_activity_id": 999999, - "field_event_contact_id": field_event_contact.id, + "field_event_participant_id": field_event_participant.id, "sample_date": "2025-01-01T14:00:00Z", "sample_name": "yet another sample name", "sample_matrix": "water", @@ -161,7 +161,6 @@ def test_409_add_sample_invalid_field_activity_id( json=payload, ) data = response.json() - print(data) assert response.status_code == 409 assert data["detail"][0]["loc"] == ["body", "field_activity_id"] assert ( @@ -196,7 +195,7 @@ def test_patch_sample(water_chemistry_sample, groundwater_level_field_activity): data = response.json() for key, value in payload.items(): - if key in ["field_event_contact_id", "field_activity_id"]: + if key in ["field_event_participant_id", "field_activity_id"]: continue assert data[key] == value