Skip to content

Commit 6fb61cf

Browse files
Merge pull request #609 from DataIntegrationGroup/jab-bdms-626-field-activity
BDMS 626: groundwater level field activity & test fixes
2 parents bf65262 + 44c598a commit 6fb61cf

3 files changed

Lines changed: 105 additions & 11 deletions

File tree

schemas/well_inventory.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
MonitoringStatus,
3535
SampleMethod,
3636
DataQuality,
37+
GroundwaterLevelReason,
3738
)
3839
from phonenumbers import NumberParseException
3940
from pydantic import (
@@ -182,6 +183,10 @@ def validator(v):
182183
DataQualityField: TypeAlias = Annotated[
183184
Optional[DataQuality], BeforeValidator(flexible_lexicon_validator(DataQuality))
184185
]
186+
GroundwaterLevelReasonField: TypeAlias = Annotated[
187+
Optional[GroundwaterLevelReason],
188+
BeforeValidator(flexible_lexicon_validator(GroundwaterLevelReason)),
189+
]
185190
PostalCodeField: TypeAlias = Annotated[
186191
Optional[str], BeforeValidator(postal_code_or_none)
187192
]
@@ -326,7 +331,7 @@ class WellInventoryRow(BaseModel):
326331
default=None,
327332
validation_alias=AliasChoices("mp_height", "mp_height_ft"),
328333
)
329-
level_status: Optional[str] = None
334+
level_status: GroundwaterLevelReasonField = None
330335
depth_to_water_ft: OptionalFloat = None
331336
data_quality: DataQualityField = None
332337
water_level_notes: Optional[str] = None

services/well_inventory_csv.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ def _find_existing_imported_well(
445445
.where(
446446
Thing.name == model.well_name_point_id,
447447
Thing.thing_type == "water well",
448-
FieldActivity.activity_type == "well inventory",
448+
FieldActivity.activity_type == "groundwater level",
449449
Sample.sample_name == sample_name,
450450
)
451451
.order_by(Thing.id.asc())
@@ -795,14 +795,23 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user)
795795
session.add(parameter)
796796
session.flush()
797797

798+
# create FieldActivity
799+
gwl_field_activity = FieldActivity(
800+
field_event=fe,
801+
activity_type="groundwater level",
802+
notes="Groundwater level measurement activity conducted during well inventory field event.",
803+
)
804+
session.add(gwl_field_activity)
805+
session.flush()
806+
798807
# create Sample
799808
sample_method = (
800809
model.sample_method.value
801810
if hasattr(model.sample_method, "value")
802811
else (model.sample_method or "Unknown")
803812
)
804813
sample = Sample(
805-
field_activity_id=fa.id,
814+
field_activity_id=gwl_field_activity.id,
806815
sample_date=model.measurement_date_time,
807816
sample_name=f"{well.name}-WL-{model.measurement_date_time.strftime('%Y%m%d%H%M')}",
808817
sample_matrix="groundwater",
@@ -813,13 +822,19 @@ def _add_csv_row(session: Session, group: Group, model: WellInventoryRow, user)
813822
session.flush()
814823

815824
# create Observation
825+
# TODO: groundwater_level_reason may be conditionally required for null depth_to_water_ft - handle accordingly
816826
observation = Observation(
817827
sample_id=sample.id,
818828
parameter_id=parameter.id,
819829
value=model.depth_to_water_ft,
820830
unit="ft",
821831
observation_datetime=model.measurement_date_time,
822832
measuring_point_height=model.mp_height,
833+
groundwater_level_reason=(
834+
model.level_status.value
835+
if hasattr(model.level_status, "value")
836+
else None
837+
),
823838
nma_data_quality=(
824839
model.data_quality.value
825840
if hasattr(model.data_quality, "value")

tests/test_well_inventory.py

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import pytest
1515
from cli.service_adapter import well_inventory_csv
1616
from core.constants import SRID_UTM_ZONE_13N, SRID_WGS84
17+
from core.enums import Role, ContactType
1718
from db import (
1819
Base,
1920
Location,
@@ -67,7 +68,7 @@ def isolate_well_inventory_tables():
6768
_reset_well_inventory_tables()
6869

6970

70-
def test_well_inventory_db_contents():
71+
def test_well_inventory_db_contents_no_waterlevels():
7172
"""
7273
Test that the well inventory upload creates the correct database contents.
7374
@@ -457,6 +458,77 @@ def test_well_inventory_db_contents():
457458
assert participant.participant.name == file_content["field_staff_2"]
458459

459460

461+
def test_well_inventory_db_contents_with_waterlevels(tmp_path):
462+
"""
463+
Tests that the following records are made:
464+
465+
- field event
466+
- field activity for well inventory
467+
- field activity for water level measurement
468+
- field participants
469+
- contact
470+
- location
471+
- thing
472+
- sample
473+
- observation
474+
475+
"""
476+
row = _minimal_valid_well_inventory_row()
477+
row.update(
478+
{
479+
"water_level_date_time": "2025-02-15T10:30:00",
480+
"depth_to_water_ft": "8",
481+
"sample_method": "Steel-tape measurement",
482+
"data_quality": "Water level accurate to within two hundreths of a foot",
483+
"water_level_notes": "Attempted measurement",
484+
"mp_height_ft": 2.5,
485+
"level_status": "Water level not affected",
486+
}
487+
)
488+
file_path = tmp_path / "well-inventory-blank-depth.csv"
489+
with file_path.open("w", encoding="utf-8", newline="") as f:
490+
writer = csv.DictWriter(f, fieldnames=list(row.keys()))
491+
writer.writeheader()
492+
writer.writerow(row)
493+
494+
result = well_inventory_csv(file_path)
495+
assert result.exit_code == 0, result.stderr
496+
497+
with session_ctx() as session:
498+
field_events = session.query(FieldEvent).all()
499+
field_activities = session.query(FieldActivity).all()
500+
field_event_participants = session.query(FieldEventParticipant).all()
501+
contacts = session.query(Contact).all()
502+
locations = session.query(Location).all()
503+
things = session.query(Thing).all()
504+
samples = session.query(Sample).all()
505+
observations = session.query(Observation).all()
506+
507+
assert len(field_events) == 1
508+
assert len(field_activities) == 2
509+
activity_types = {fa.activity_type for fa in field_activities}
510+
assert activity_types == {
511+
"well inventory",
512+
"groundwater level",
513+
}, f"Unexpected activity types: {activity_types}"
514+
gwl_field_activity = next(
515+
(fa for fa in field_activities if fa.activity_type == "groundwater level"),
516+
None,
517+
)
518+
assert gwl_field_activity is not None
519+
520+
assert len(field_event_participants) == 1
521+
assert len(contacts) == 1
522+
assert len(locations) == 1
523+
assert len(things) == 1
524+
assert len(samples) == 1
525+
sample = samples[0]
526+
assert sample.field_activity == gwl_field_activity
527+
assert len(observations) == 1
528+
observation = observations[0]
529+
assert observation.sample == sample
530+
531+
460532
def test_blank_depth_to_water_still_creates_water_level_records(tmp_path):
461533
"""Blank depth-to-water is treated as missing while preserving the attempted measurement."""
462534
row = _minimal_valid_well_inventory_row()
@@ -486,9 +558,9 @@ def test_blank_depth_to_water_still_creates_water_level_records(tmp_path):
486558

487559
assert len(samples) == 1
488560
assert len(observations) == 1
489-
assert samples[0].sample_date == datetime.fromisoformat("2025-02-15T10:30:00")
561+
assert samples[0].sample_date == datetime.fromisoformat("2025-02-15T10:30:00Z")
490562
assert observations[0].observation_datetime == datetime.fromisoformat(
491-
"2025-02-15T10:30:00"
563+
"2025-02-15T10:30:00Z"
492564
)
493565
assert observations[0].value is None
494566
assert observations[0].measuring_point_height == 2.5
@@ -779,8 +851,8 @@ def test_make_contact_with_full_info(self):
779851
model.contact_special_requests_notes = "Call before visiting"
780852
model.contact_1_name = "John Doe"
781853
model.contact_1_organization = "Test Org"
782-
model.contact_1_role = "Owner"
783-
model.contact_1_type = "Primary"
854+
model.contact_1_role = Role.Owner
855+
model.contact_1_type = ContactType.Primary
784856
model.contact_1_email_1 = "john@example.com"
785857
model.contact_1_email_1_type = "Work"
786858
model.contact_1_email_2 = None
@@ -866,8 +938,8 @@ def test_make_contact_with_organization_only(self):
866938
model.contact_special_requests_notes = None
867939
model.contact_1_name = None
868940
model.contact_1_organization = "Test Org"
869-
model.contact_1_role = None
870-
model.contact_1_type = None
941+
model.contact_1_role = Role.Owner
942+
model.contact_1_type = ContactType.Primary
871943
model.contact_1_email_1 = None
872944
model.contact_1_email_1_type = None
873945
model.contact_1_email_2 = None
@@ -1094,7 +1166,7 @@ def test_water_level_aliases_are_mapped(self):
10941166
"sample_method": "Steel-tape measurement",
10951167
"water_level_date_time": "2025-02-15T10:30:00",
10961168
"mp_height_ft": 2.5,
1097-
"level_status": "Static",
1169+
"level_status": "Other conditions exist that would affect the level (remarks)",
10981170
"depth_to_water_ft": 11.2,
10991171
"data_quality": "Water level accurate to within two hundreths of a foot",
11001172
"water_level_notes": "Initial reading",
@@ -1131,6 +1203,8 @@ def test_blank_contact_organization_is_treated_as_none(self):
11311203
row = _minimal_valid_well_inventory_row()
11321204
row["contact_1_name"] = "Test Contact"
11331205
row["contact_1_organization"] = ""
1206+
row["contact_1_role"] = "Owner"
1207+
row["contact_1_type"] = "Primary"
11341208

11351209
model = WellInventoryRow(**row)
11361210

0 commit comments

Comments
 (0)