From 1dfc24dce393d026e6b44bf63c88291f8981d774 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 10:48:32 -0600 Subject: [PATCH 1/9] feat(well inventory): add groundwater level field activity for well inventory import This commit adds a new field activity for groundwater level measurements during the well inventory import process if an optional water level is provided. The field activity is created with the type "groundwater level" and includes notes about the measurement. This enhancement allows for better tracking of groundwater level data associated with well inventory events. --- schemas/well_inventory.py | 7 +++- services/well_inventory_csv.py | 12 +++++- tests/test_well_inventory.py | 67 +++++++++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/schemas/well_inventory.py b/schemas/well_inventory.py index 5bf54787..c3554060 100644 --- a/schemas/well_inventory.py +++ b/schemas/well_inventory.py @@ -34,6 +34,7 @@ MonitoringStatus, SampleMethod, DataQuality, + GroundwaterLevelReason, ) from phonenumbers import NumberParseException from pydantic import ( @@ -182,6 +183,10 @@ def validator(v): DataQualityField: TypeAlias = Annotated[ Optional[DataQuality], BeforeValidator(flexible_lexicon_validator(DataQuality)) ] +GroundwaterLevelReasonField: TypeAlias = Annotated[ + Optional[GroundwaterLevelReason], + BeforeValidator(flexible_lexicon_validator(GroundwaterLevelReason)), +] PostalCodeField: TypeAlias = Annotated[ Optional[str], BeforeValidator(postal_code_or_none) ] @@ -326,7 +331,7 @@ class WellInventoryRow(BaseModel): default=None, validation_alias=AliasChoices("mp_height", "mp_height_ft"), ) - level_status: Optional[str] = None + level_status: GroundwaterLevelReasonField = None depth_to_water_ft: OptionalFloat = None data_quality: DataQualityField = None water_level_notes: Optional[str] = None diff --git a/services/well_inventory_csv.py b/services/well_inventory_csv.py index 94c348dd..0c567f3b 100644 --- a/services/well_inventory_csv.py +++ b/services/well_inventory_csv.py @@ -795,6 +795,15 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) session.add(parameter) session.flush() + # create FieldActivity + gwl_field_activity = FieldActivity( + field_event=fe, + activity_type="groundwater level", + notes="Groundwater level measurement activity conducted during well inventory field event.", + ) + session.add(gwl_field_activity) + session.flush() + # create Sample sample_method = ( model.sample_method.value @@ -802,7 +811,7 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) else (model.sample_method or "Unknown") ) sample = Sample( - field_activity_id=fa.id, + field_activity_id=gwl_field_activity.id, sample_date=model.measurement_date_time, sample_name=f"{well.name}-WL-{model.measurement_date_time.strftime('%Y%m%d%H%M')}", sample_matrix="groundwater", @@ -820,6 +829,7 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) unit="ft", observation_datetime=model.measurement_date_time, measuring_point_height=model.mp_height, + groundwater_level_reason=model.level_status, nma_data_quality=( model.data_quality.value if hasattr(model.data_quality, "value") diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index 8ba59be3..0fa4f305 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -67,7 +67,7 @@ def isolate_well_inventory_tables(): _reset_well_inventory_tables() -def test_well_inventory_db_contents(): +def test_well_inventory_db_contents_no_waterlevels(): """ Test that the well inventory upload creates the correct database contents. @@ -457,6 +457,71 @@ def test_well_inventory_db_contents(): assert participant.participant.name == file_content["field_staff_2"] +def test_well_inventory_db_contents_with_waterlevels(tmp_path): + """ + Tests that the following records are made: + + - field event + - field activity for well inventory + - field activity for water level measurement + - field participants + - contact + - location + - thing + - sample + - observation + + """ + row = _minimal_valid_well_inventory_row() + row.update( + { + "water_level_date_time": "2025-02-15T10:30:00", + "depth_to_water_ft": "8", + "sample_method": "Steel-tape measurement", + "data_quality": "Water level accurate to within two hundreths of a foot", + "water_level_notes": "Attempted measurement", + "mp_height_ft": 2.5, + "level_status": "Water level not affected", + } + ) + file_path = tmp_path / "well-inventory-blank-depth.csv" + with file_path.open("w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=list(row.keys())) + writer.writeheader() + writer.writerow(row) + + result = well_inventory_csv(file_path) + assert result.exit_code == 0, result.stderr + + with session_ctx() as session: + field_events = session.query(FieldEvent).all() + field_activities = session.query(FieldActivity).all() + field_event_participants = session.query(FieldEventParticipant).all() + contacts = session.query(Contact).all() + locations = session.query(Location).all() + things = session.query(Thing).all() + samples = session.query(Sample).all() + observations = session.query(Observation).all() + + assert len(field_events) == 1 + assert len(field_activities) == 2 + assert len(field_event_participants) == 1 + assert len(contacts) == 1 + assert len(locations) == 1 + assert len(things) == 1 + assert len(samples) == 1 + assert len(observations) == 1 + + session.query(FieldEvent).delete() + session.query(FieldActivity).delete() + session.query(FieldEventParticipant).delete() + session.query(Contact).delete() + session.query(Location).delete() + session.query(Thing).delete() + session.query(Sample).delete() + session.query(Observation).delete() + + def test_blank_depth_to_water_still_creates_water_level_records(tmp_path): """Blank depth-to-water is treated as missing while preserving the attempted measurement.""" row = _minimal_valid_well_inventory_row() From fe9fc0ddccf514bc6cef16a4349e147516003bb1 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 11:04:51 -0600 Subject: [PATCH 2/9] fix(test): compare dt aware objects for optional water level tests --- tests/test_well_inventory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index 0fa4f305..4d30eb32 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -551,9 +551,9 @@ def test_blank_depth_to_water_still_creates_water_level_records(tmp_path): assert len(samples) == 1 assert len(observations) == 1 - assert samples[0].sample_date == datetime.fromisoformat("2025-02-15T10:30:00") + assert samples[0].sample_date == datetime.fromisoformat("2025-02-15T10:30:00Z") assert observations[0].observation_datetime == datetime.fromisoformat( - "2025-02-15T10:30:00" + "2025-02-15T10:30:00Z" ) assert observations[0].value is None assert observations[0].measuring_point_height == 2.5 From 0c9e8faeada23c4a5e2b7eb194d5fbbd18336351 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 11:11:34 -0600 Subject: [PATCH 3/9] fix(test): use enums when testing helper functions --- tests/test_well_inventory.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index 4d30eb32..34bf3bee 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -14,6 +14,7 @@ import pytest from cli.service_adapter import well_inventory_csv from core.constants import SRID_UTM_ZONE_13N, SRID_WGS84 +from core.enums import Role, ContactType from db import ( Base, Location, @@ -844,8 +845,8 @@ def test_make_contact_with_full_info(self): model.contact_special_requests_notes = "Call before visiting" model.contact_1_name = "John Doe" model.contact_1_organization = "Test Org" - model.contact_1_role = "Owner" - model.contact_1_type = "Primary" + model.contact_1_role = Role.Owner + model.contact_1_type = ContactType.Primary model.contact_1_email_1 = "john@example.com" model.contact_1_email_1_type = "Work" model.contact_1_email_2 = None @@ -931,8 +932,8 @@ def test_make_contact_with_organization_only(self): model.contact_special_requests_notes = None model.contact_1_name = None model.contact_1_organization = "Test Org" - model.contact_1_role = None - model.contact_1_type = None + model.contact_1_role = Role.Owner + model.contact_1_type = ContactType.Primary model.contact_1_email_1 = None model.contact_1_email_1_type = None model.contact_1_email_2 = None From 815cfc62a94aaba194fec042dff402e6e357d590 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 12:04:17 -0600 Subject: [PATCH 4/9] fix(test): utilize autouse fixture to clean up tests --- tests/test_well_inventory.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index 34bf3bee..91a87c41 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -513,15 +513,6 @@ def test_well_inventory_db_contents_with_waterlevels(tmp_path): assert len(samples) == 1 assert len(observations) == 1 - session.query(FieldEvent).delete() - session.query(FieldActivity).delete() - session.query(FieldEventParticipant).delete() - session.query(Contact).delete() - session.query(Location).delete() - session.query(Thing).delete() - session.query(Sample).delete() - session.query(Observation).delete() - def test_blank_depth_to_water_still_creates_water_level_records(tmp_path): """Blank depth-to-water is treated as missing while preserving the attempted measurement.""" From 3e9dcf39f228065a71beedfdda6467271cfe5720 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 12:05:58 -0600 Subject: [PATCH 5/9] fix(test): fix failing well inventory tests --- tests/test_well_inventory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index 91a87c41..f38456c9 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -1151,7 +1151,7 @@ def test_water_level_aliases_are_mapped(self): "sample_method": "Steel-tape measurement", "water_level_date_time": "2025-02-15T10:30:00", "mp_height_ft": 2.5, - "level_status": "Static", + "level_status": "Other conditions exist that would affect the level (remarks)", "depth_to_water_ft": 11.2, "data_quality": "Water level accurate to within two hundreths of a foot", "water_level_notes": "Initial reading", @@ -1188,6 +1188,8 @@ def test_blank_contact_organization_is_treated_as_none(self): row = _minimal_valid_well_inventory_row() row["contact_1_name"] = "Test Contact" row["contact_1_organization"] = "" + row["contact_1_role"] = "Owner" + row["contact_1_type"] = "Primary" model = WellInventoryRow(**row) From d6e1dc4c56a3fb8fad6396a92b0b0af71e355acf Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 12:12:32 -0600 Subject: [PATCH 6/9] fix(well inventory): use correct activity type for water level records If a sample is recorded use the field activity with activity type "groundwater level" instead of "well inventory", otherwise use "well inventory" for the field activity. --- services/well_inventory_csv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/well_inventory_csv.py b/services/well_inventory_csv.py index 0c567f3b..612adbeb 100644 --- a/services/well_inventory_csv.py +++ b/services/well_inventory_csv.py @@ -445,7 +445,7 @@ def _find_existing_imported_well( .where( Thing.name == model.well_name_point_id, Thing.thing_type == "water well", - FieldActivity.activity_type == "well inventory", + FieldActivity.activity_type == "groundwater level", Sample.sample_name == sample_name, ) .order_by(Thing.id.asc()) From b2bc17dfec99e3c17625b20865fde049126f9044 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 12:21:24 -0600 Subject: [PATCH 7/9] fix(well inventory): retrieve groundwater level reason enum value, else None This protects the field for when null value are submitted --- services/well_inventory_csv.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/well_inventory_csv.py b/services/well_inventory_csv.py index 612adbeb..049eddea 100644 --- a/services/well_inventory_csv.py +++ b/services/well_inventory_csv.py @@ -822,6 +822,7 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) session.flush() # create Observation + # TODO: groundwater_level_reason may be conditionally required for null depth_to_water_ft - handle accordingly observation = Observation( sample_id=sample.id, parameter_id=parameter.id, @@ -829,7 +830,11 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user) unit="ft", observation_datetime=model.measurement_date_time, measuring_point_height=model.mp_height, - groundwater_level_reason=model.level_status, + groundwater_level_reason=( + model.level_status.value + if hasattr(model.level_status, "value") + else None + ), nma_data_quality=( model.data_quality.value if hasattr(model.data_quality, "value") From e899412b4f5d038f2c2fd69f11c767e932c8a4af Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 12:24:20 -0600 Subject: [PATCH 8/9] fix(test): ensure sample references correct field activity --- tests/test_well_inventory.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index f38456c9..16f8bf01 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -506,11 +506,19 @@ def test_well_inventory_db_contents_with_waterlevels(tmp_path): assert len(field_events) == 1 assert len(field_activities) == 2 + gwl_field_activity = next( + (fa for fa in field_activities if fa.activity_type == "groundwater level"), + None, + ) + assert gwl_field_activity is not None + assert len(field_event_participants) == 1 assert len(contacts) == 1 assert len(locations) == 1 assert len(things) == 1 assert len(samples) == 1 + sample = samples[0] + assert sample.field_activity == gwl_field_activity assert len(observations) == 1 From 44c598ab822706436b3995c5ed05a82c9ccc159e Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Wed, 18 Mar 2026 12:26:31 -0600 Subject: [PATCH 9/9] feat(test): ensure more robust water level tests --- tests/test_well_inventory.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_well_inventory.py b/tests/test_well_inventory.py index 16f8bf01..08d2573b 100644 --- a/tests/test_well_inventory.py +++ b/tests/test_well_inventory.py @@ -506,6 +506,11 @@ def test_well_inventory_db_contents_with_waterlevels(tmp_path): assert len(field_events) == 1 assert len(field_activities) == 2 + activity_types = {fa.activity_type for fa in field_activities} + assert activity_types == { + "well inventory", + "groundwater level", + }, f"Unexpected activity types: {activity_types}" gwl_field_activity = next( (fa for fa in field_activities if fa.activity_type == "groundwater level"), None, @@ -520,6 +525,8 @@ def test_well_inventory_db_contents_with_waterlevels(tmp_path): sample = samples[0] assert sample.field_activity == gwl_field_activity assert len(observations) == 1 + observation = observations[0] + assert observation.sample == sample def test_blank_depth_to_water_still_creates_water_level_records(tmp_path):