From 3cf9cd8d6f0f7f2a13e4e2a1268833039d3bbec0 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Mon, 8 Dec 2025 16:23:34 -0700 Subject: [PATCH 1/3] feat: add "id" field to the parameters contact search results Adding the id field will enable the frontend to allow users to click on a searched contact and go to the contact detail page --- api/search.py | 3 +-- tests/test_search.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/search.py b/api/search.py index 7f8c3786b..7d42d4aa9 100644 --- a/api/search.py +++ b/api/search.py @@ -61,8 +61,7 @@ def _get_contact_results(session: Session, q: str, limit: int) -> list[dict]: "email": [e.email for e in c.emails], "phone": [p.phone_number for p in c.phones], "address": [a.address_line_1 for a in c.addresses], - # 'address': c.address, - # 'location_id': c.location_id + "id": c.id, }, } for c in contacts diff --git a/tests/test_search.py b/tests/test_search.py index acf45c29e..a94ce9b59 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -75,6 +75,7 @@ def test_search_contact(contact): queried_contact = session.scalars(query).first() assert queried_contact is not None + assert queried_contact.id == contact.id def test_search_contact_no_results(contact): From a76a18c37ca63a4d8a5a2f2c3e411cda5e7f3bb5 Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Mon, 8 Dec 2025 16:45:27 -0700 Subject: [PATCH 2/3] fix: remove depricated well_purpose field from the Thing model This information is now housed in the WellPurpose model, which has a 1:M relationship with Thing. When this refactor occurred we never updated the search endpoint to reflect this change. --- api/search.py | 10 +++++++--- db/thing.py | 4 ---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/search.py b/api/search.py index 7d42d4aa9..2a15be397 100644 --- a/api/search.py +++ b/api/search.py @@ -17,7 +17,7 @@ from fastapi_pagination import paginate from fastapi_pagination.utils import disable_installed_extensions_check from sqlalchemy import select, func, text -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, selectinload from api.pagination import CustomPage from core.dependencies import session_dependency, viewer_dependency @@ -80,7 +80,11 @@ def _get_thing_results(session: Session, q: str, limit: int) -> list[dict]: select(Thing) .outerjoin(WellCasingMaterial) .outerjoin(WellPurpose) - .where(Thing.thing_type == "water well"), + .where(Thing.thing_type == "water well") + .options( + selectinload(Thing.well_casing_materials), + selectinload(Thing.well_purposes), + ), q, vector=well_vector, limit=limit, @@ -116,7 +120,7 @@ def make_well_response(thing: Thing) -> dict: "Wells", thing, { - "well_purpose": thing.well_purpose, + "well_purposes": [wp.purpose for wp in thing.well_purposes], "well_depth": thing.well_depth, "hole_depth": thing.hole_depth, }, diff --git a/db/thing.py b/db/thing.py index 286372242..0015cfa2b 100644 --- a/db/thing.py +++ b/db/thing.py @@ -100,10 +100,6 @@ class Thing( info={"unit": "feet below ground surface"}, comment="Depth of the drilled hole, from ground surface to the bottom of the borehole (in feet).", ) - well_purpose: Mapped[str] = lexicon_term( - nullable=True, - comment="A controlled vocabulary field defining the primary function of the well (e.g., 'Monitoring', 'Irrigation', 'Domestic', 'Livestock', 'Remediation').", - ) well_casing_diameter: Mapped[float] = mapped_column( Float, nullable=True, From 3b53019e5e98b5d7d5b8d0deec64a4e653262abf Mon Sep 17 00:00:00 2001 From: jacob-a-brown Date: Tue, 9 Dec 2025 08:36:51 -0700 Subject: [PATCH 3/3] feat: include id and things in contact search results Adding the contact's id enables the frontend to quickly navigate to its detail view. Including associated things provides context about the contact's relationships, enhancing the search experience. Since the ID is also included the frontend can use it to go to the thing's detail page. --- api/search.py | 18 +++++++++++++++++- tests/test_search.py | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/api/search.py b/api/search.py index 2a15be397..ab4c28c0f 100644 --- a/api/search.py +++ b/api/search.py @@ -26,6 +26,7 @@ Email, Phone, Address, + ThingContactAssociation, Thing, WellCasingMaterial, WellPurpose, @@ -47,7 +48,18 @@ def _get_contact_results(session: Session, q: str, limit: int) -> list[dict]: ) query = search( - select(Contact).outerjoin(Email).outerjoin(Phone).outerjoin(Address), + select(Contact) + .outerjoin(Email) + .outerjoin(Phone) + .outerjoin(Address) + .options( + selectinload(Contact.emails), + selectinload(Contact.phones), + selectinload(Contact.addresses), + selectinload(Contact.thing_associations).selectinload( + ThingContactAssociation.thing + ), + ), q, vector=vector, limit=limit, @@ -61,6 +73,10 @@ def _get_contact_results(session: Session, q: str, limit: int) -> list[dict]: "email": [e.email for e in c.emails], "phone": [p.phone_number for p in c.phones], "address": [a.address_line_1 for a in c.addresses], + "things": [ + {"label": t.name, "id": t.id, "thing_type": t.thing_type} + for t in c.things + ], "id": c.id, }, } diff --git a/tests/test_search.py b/tests/test_search.py index a94ce9b59..137f82263 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -48,6 +48,20 @@ def test_search_api(water_well_thing, spring_thing, contact): assert isinstance(items, list) assert len(items) == 3 + # Check the contacts returned + contact_items = [item for item in items if item["group"] == "Contacts"] + assert len(contact_items) == 1 + contact_item = contact_items[0] + assert contact_item["label"] == contact.name + assert contact_item["properties"]["id"] == contact.id + assert contact_item["properties"]["things"] == [ + { + "label": water_well_thing.name, + "id": water_well_thing.id, + "thing_type": water_well_thing.thing_type, + }, + ] + @pytest.mark.skip(reason="This test is not working .") def test_search_api2(): @@ -75,7 +89,6 @@ def test_search_contact(contact): queried_contact = session.scalars(query).first() assert queried_contact is not None - assert queried_contact.id == contact.id def test_search_contact_no_results(contact):