From c41c4873cd39ef5f9b0a5449c73258f4021111ff Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Mon, 26 Jan 2026 15:50:49 -0700 Subject: [PATCH 01/12] refactor: update name and label for Minor Trace Chemistry to include 'NMA' prefix --- admin/views/minor_trace_chemistry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/views/minor_trace_chemistry.py b/admin/views/minor_trace_chemistry.py index 3db6e8a08..15eaee810 100644 --- a/admin/views/minor_trace_chemistry.py +++ b/admin/views/minor_trace_chemistry.py @@ -33,8 +33,8 @@ class MinorTraceChemistryAdmin(OcotilloModelView): # ========== Basic Configuration ========== identity = "n-m-a_-minor-trace-chemistry" - name = "Minor Trace Chemistry" - label = "Minor Trace Chemistry" + name = "NMA Minor Trace Chemistry" + label = "NMA Minor Trace Chemistry" icon = "fa fa-flask" pk_attr = "global_id" pk_type = uuid.UUID From 0376951ae3df2da3caa1b989f851ab7d34e23d09 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Mon, 26 Jan 2026 16:55:52 -0700 Subject: [PATCH 02/12] refactor: rename chemistry_sample_info_id to sample_pt_id in NMA_MinorTraceChemistry This aligns with the 1:1 migration, preserving all legacy field names. --- db/nma_legacy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/nma_legacy.py b/db/nma_legacy.py index 72f398040..7d6ab7d00 100644 --- a/db/nma_legacy.py +++ b/db/nma_legacy.py @@ -475,7 +475,7 @@ class NMA_MinorTraceChemistry(Base): __tablename__ = "NMA_MinorTraceChemistry" __table_args__ = ( UniqueConstraint( - "chemistry_sample_info_id", + "sample_pt_id", "analyte", name="uq_minor_trace_chemistry_sample_analyte", ), @@ -486,7 +486,7 @@ class NMA_MinorTraceChemistry(Base): ) # FK to ChemistrySampleInfo - required (no orphans) - chemistry_sample_info_id: Mapped[uuid.UUID] = mapped_column( + sample_pt_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("NMA_Chemistry_SampleInfo.SamplePtID", ondelete="CASCADE"), nullable=False, @@ -510,8 +510,8 @@ class NMA_MinorTraceChemistry(Base): "NMA_Chemistry_SampleInfo", back_populates="minor_trace_chemistries" ) - @validates("chemistry_sample_info_id") - def validate_chemistry_sample_info_id(self, key, value): + @validates("sample_pt_id") + def validate_sample_pt_id(self, key, value): """Prevent orphan NMA_MinorTraceChemistry - must have a parent ChemistrySampleInfo.""" if value is None: raise ValueError( From 6c0fa8b072281c661a29e9da9c563912e1d19f33 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Mon, 26 Jan 2026 17:09:32 -0700 Subject: [PATCH 03/12] refactor (models): add missing legacy fields and update column mappings for consistency and clarity --- db/nma_legacy.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/db/nma_legacy.py b/db/nma_legacy.py index 7d6ab7d00..004fbf72c 100644 --- a/db/nma_legacy.py +++ b/db/nma_legacy.py @@ -493,17 +493,26 @@ class NMA_MinorTraceChemistry(Base): ) # Legacy columns - analyte: Mapped[Optional[str]] = mapped_column(String(50)) - sample_value: Mapped[Optional[float]] = mapped_column(Float) - units: Mapped[Optional[str]] = mapped_column(String(20)) - symbol: Mapped[Optional[str]] = mapped_column(String(10)) - analysis_method: Mapped[Optional[str]] = mapped_column(String(100)) - analysis_date: Mapped[Optional[date]] = mapped_column(Date) - notes: Mapped[Optional[str]] = mapped_column(Text) - analyses_agency: Mapped[Optional[str]] = mapped_column(String(100)) - uncertainty: Mapped[Optional[float]] = mapped_column(Float) - volume: Mapped[Optional[int]] = mapped_column(Integer) - volume_unit: Mapped[Optional[str]] = mapped_column(String(20)) + sample_point_id: Mapped[Optional[str]] = mapped_column("SamplePointID", String(10)) + analyte: Mapped[Optional[str]] = mapped_column("Analyte", String(50)) + symbol: Mapped[Optional[str]] = mapped_column("Symbol", String(50)) + sample_value: Mapped[Optional[float]] = mapped_column( + "SampleValue", Float, server_default=text("0") + ) + units: Mapped[Optional[str]] = mapped_column("Units", String(50)) + uncertainty: Mapped[Optional[float]] = mapped_column("Uncertainty", Float) + analysis_method: Mapped[Optional[str]] = mapped_column( + "AnalysisMethod", String(255) + ) + analysis_date: Mapped[Optional[datetime]] = mapped_column("AnalysisDate", DateTime) + notes: Mapped[Optional[str]] = mapped_column("Notes", String(255)) + volume: Mapped[Optional[int]] = mapped_column( + "Volume", Integer, server_default=text("0") + ) + volume_unit: Mapped[Optional[str]] = mapped_column("VolumeUnit", String(50)) + object_id: Mapped[Optional[int]] = mapped_column("OBJECTID", Integer, unique=True) + analyses_agency: Mapped[Optional[str]] = mapped_column("AnalysesAgency", String(50)) + wclab_id: Mapped[Optional[str]] = mapped_column("WCLab_ID", String(25)) # --- Relationships --- chemistry_sample_info: Mapped["NMA_Chemistry_SampleInfo"] = relationship( From 5cea4952c549a00edf414e1f0249746d880e1bf3 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Mon, 26 Jan 2026 17:31:44 -0700 Subject: [PATCH 04/12] refactor: updated admin view reflect the renamed/added fields from the model, and aligned the admin tests with the new configuration. Details: - Added sample_pt_id, sample_point_id, object_id, and wclab_id to list/sort/search/form configs in admin/views/minor_trace_chemistry.py. - Updated field labels to match legacy column naming. - Adjusted expectations in tests/test_admin_minor_trace_chemistry.py to match the new fields/labels. --- admin/views/minor_trace_chemistry.py | 28 +++++++++++++++++++---- tests/test_admin_minor_trace_chemistry.py | 10 ++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/admin/views/minor_trace_chemistry.py b/admin/views/minor_trace_chemistry.py index 15eaee810..aa9d1a64d 100644 --- a/admin/views/minor_trace_chemistry.py +++ b/admin/views/minor_trace_chemistry.py @@ -53,6 +53,8 @@ def can_delete(self, request: Request) -> bool: list_fields = [ "global_id", HasOne("chemistry_sample_info", identity="n-m-a_-chemistry_-sample-info"), + "sample_pt_id", + "sample_point_id", "analyte", "sample_value", "units", @@ -63,23 +65,31 @@ def can_delete(self, request: Request) -> bool: sortable_fields = [ "global_id", + "sample_pt_id", + "sample_point_id", "analyte", "sample_value", "units", "symbol", "analysis_date", "analyses_agency", + "wclab_id", + "object_id", ] fields_default_sort = [("analysis_date", True)] searchable_fields = [ "global_id", + "sample_pt_id", + "sample_point_id", "analyte", "symbol", "analysis_method", + "analysis_date", "notes", "analyses_agency", + "wclab_id", ] page_size = 50 @@ -90,6 +100,8 @@ def can_delete(self, request: Request) -> bool: fields = [ "global_id", HasOne("chemistry_sample_info", identity="n-m-a_-chemistry_-sample-info"), + "sample_pt_id", + "sample_point_id", "analyte", "symbol", "sample_value", @@ -100,23 +112,29 @@ def can_delete(self, request: Request) -> bool: "notes", "volume", "volume_unit", + "object_id", "analyses_agency", + "wclab_id", ] field_labels = { "global_id": "GlobalID", "chemistry_sample_info": "Chemistry Sample Info", + "sample_pt_id": "SamplePtID", + "sample_point_id": "SamplePointID", "analyte": "Analyte", "symbol": "Symbol", - "sample_value": "Sample Value", + "sample_value": "SampleValue", "units": "Units", "uncertainty": "Uncertainty", - "analysis_method": "Analysis Method", - "analysis_date": "Analysis Date", + "analysis_method": "AnalysisMethod", + "analysis_date": "AnalysisDate", "notes": "Notes", "volume": "Volume", - "volume_unit": "Volume Unit", - "analyses_agency": "Analyses Agency", + "volume_unit": "VolumeUnit", + "object_id": "OBJECTID", + "analyses_agency": "AnalysesAgency", + "wclab_id": "WCLab_ID", } diff --git a/tests/test_admin_minor_trace_chemistry.py b/tests/test_admin_minor_trace_chemistry.py index 9777d0c8d..d80e6f223 100644 --- a/tests/test_admin_minor_trace_chemistry.py +++ b/tests/test_admin_minor_trace_chemistry.py @@ -108,6 +108,8 @@ def test_list_fields_include_required_columns(self, view): required_columns = [ "global_id", "chemistry_sample_info", # HasOne relationship to parent + "sample_pt_id", + "sample_point_id", "analyte", "sample_value", "units", @@ -146,6 +148,8 @@ def test_form_includes_all_chemistry_fields(self): # Note: chemistry_sample_info is a HasOne field, not a string expected_string_fields = [ "global_id", + "sample_pt_id", + "sample_point_id", "analyte", "symbol", "sample_value", @@ -156,7 +160,9 @@ def test_form_includes_all_chemistry_fields(self): "notes", "volume", "volume_unit", + "object_id", "analyses_agency", + "wclab_id", ] configured_fields = MinorTraceChemistryAdmin.fields @@ -176,8 +182,8 @@ def test_form_includes_all_chemistry_fields(self): def test_field_labels_are_human_readable(self, view): """Field labels should be human-readable.""" assert view.field_labels.get("global_id") == "GlobalID" - assert view.field_labels.get("sample_value") == "Sample Value" - assert view.field_labels.get("analysis_date") == "Analysis Date" + assert view.field_labels.get("sample_value") == "SampleValue" + assert view.field_labels.get("analysis_date") == "AnalysisDate" def test_searchable_fields_include_key_fields(self, view): """Searchable fields should include commonly searched columns.""" From 7af63bee759d14036374f519ed1caaa2afffff6f Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Mon, 26 Jan 2026 17:42:04 -0700 Subject: [PATCH 05/12] fix: fixed UniqueConstraint to reference the column name SQLAlchemy expects ("Analyte"). --- db/nma_legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/nma_legacy.py b/db/nma_legacy.py index 004fbf72c..6f9b4e35f 100644 --- a/db/nma_legacy.py +++ b/db/nma_legacy.py @@ -476,7 +476,7 @@ class NMA_MinorTraceChemistry(Base): __table_args__ = ( UniqueConstraint( "sample_pt_id", - "analyte", + "Analyte", name="uq_minor_trace_chemistry_sample_analyte", ), ) From 948719332a6ca0116f9ea67b077b6bfb8e260f4c Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Tue, 27 Jan 2026 10:20:32 -0700 Subject: [PATCH 06/12] refactor: update admin tests to reflect 'NMA' prefix in Minor Trace Chemistry view --- tests/test_admin_minor_trace_chemistry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_admin_minor_trace_chemistry.py b/tests/test_admin_minor_trace_chemistry.py index d80e6f223..fbc4937d8 100644 --- a/tests/test_admin_minor_trace_chemistry.py +++ b/tests/test_admin_minor_trace_chemistry.py @@ -37,15 +37,15 @@ def test_minor_trace_chemistry_view_is_registered(self): admin = create_admin(app) view_names = [v.name for v in admin._views] - assert "Minor Trace Chemistry" in view_names, ( - f"Expected 'Minor Trace Chemistry' to be registered in admin views. " + assert "NMA Minor Trace Chemistry" in view_names, ( + f"Expected 'NMA Minor Trace Chemistry' to be registered in admin views. " f"Found: {view_names}" ) def test_view_has_correct_label(self): """View should have proper label for sidebar display.""" view = MinorTraceChemistryAdmin(NMA_MinorTraceChemistry) - assert view.label == "Minor Trace Chemistry" + assert view.label == "NMA Minor Trace Chemistry" def test_class_has_flask_icon_configured(self): """View class should have flask icon configured for chemistry data.""" From 338abd4730844a006019c00c70568fb8b69c3546 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Tue, 27 Jan 2026 11:54:53 -0700 Subject: [PATCH 07/12] refactor: add 'wclab_id' and 'object_id' to sortable fields in Minor Trace Chemistry view --- admin/views/minor_trace_chemistry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/admin/views/minor_trace_chemistry.py b/admin/views/minor_trace_chemistry.py index aa9d1a64d..84f02bfdf 100644 --- a/admin/views/minor_trace_chemistry.py +++ b/admin/views/minor_trace_chemistry.py @@ -61,6 +61,8 @@ def can_delete(self, request: Request) -> bool: "symbol", "analysis_date", "analyses_agency", + "wclab_id", + "object_id", ] sortable_fields = [ From 34f157dfcb22e271c97378cf0c6481bd50d7b391 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Tue, 27 Jan 2026 12:57:27 -0700 Subject: [PATCH 08/12] fix: align minor trace chemistry schema and tests - map SamplePtID in NMA_MinorTraceChemistry and fix unique constraint - update minor trace admin integration test for NMA label/sample_pt_id - add alembic migration to align legacy column names and merge heads --- ...c1f5b7d2e_align_nma_minor_trace_columns.py | 134 ++++++++++++++++++ ..._merge_minor_trace_and_field_parameters.py | 27 ++++ db/nma_legacy.py | 3 +- .../test_admin_minor_trace_chemistry.py | 4 +- 4 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py create mode 100644 alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py diff --git a/alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py b/alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py new file mode 100644 index 000000000..0b625144e --- /dev/null +++ b/alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py @@ -0,0 +1,134 @@ +"""Align NMA_MinorTraceChemistry columns with legacy schema. + +Revision ID: 3a9c1f5b7d2e +Revises: 2d67da5ff3ae +Create Date: 2026-01-31 12:00:00.000000 +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect + +# revision identifiers, used by Alembic. +revision: str = "3a9c1f5b7d2e" +down_revision: Union[str, Sequence[str], None] = "2d67da5ff3ae" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _column_names(inspector, table_name: str) -> set[str]: + return {col["name"] for col in inspector.get_columns(table_name)} + + +def upgrade() -> None: + """Rename legacy columns and add missing fields.""" + bind = op.get_bind() + inspector = inspect(bind) + if not inspector.has_table("NMA_MinorTraceChemistry"): + return + + table_name = "NMA_MinorTraceChemistry" + columns = _column_names(inspector, table_name) + + rename_map = { + "chemistry_sample_info_id": "SamplePtID", + "sample_point_id": "SamplePointID", + "analyte": "Analyte", + "sample_value": "SampleValue", + "units": "Units", + "symbol": "Symbol", + "analysis_method": "AnalysisMethod", + "analysis_date": "AnalysisDate", + "notes": "Notes", + "analyses_agency": "AnalysesAgency", + "uncertainty": "Uncertainty", + "volume": "Volume", + "volume_unit": "VolumeUnit", + } + + for old_name, new_name in rename_map.items(): + if old_name in columns and new_name not in columns: + op.alter_column(table_name, old_name, new_column_name=new_name) + columns.remove(old_name) + columns.add(new_name) + + if "SamplePointID" not in columns: + op.add_column( + table_name, sa.Column("SamplePointID", sa.String(length=10), nullable=True) + ) + if "OBJECTID" not in columns: + op.add_column(table_name, sa.Column("OBJECTID", sa.Integer(), nullable=True)) + if "WCLab_ID" not in columns: + op.add_column( + table_name, sa.Column("WCLab_ID", sa.String(length=25), nullable=True) + ) + + unique_constraints = inspector.get_unique_constraints(table_name) + unique_columns = {tuple(uc.get("column_names") or []) for uc in unique_constraints} + unique_names = {uc.get("name") for uc in unique_constraints} + + if ( + ("OBJECTID",) not in unique_columns + and "uq_nma_minor_trace_chemistry_objectid" not in unique_names + ): + op.create_unique_constraint( + "uq_nma_minor_trace_chemistry_objectid", + table_name, + ["OBJECTID"], + ) + + if "uq_minor_trace_chemistry_sample_analyte" not in unique_names: + op.create_unique_constraint( + "uq_minor_trace_chemistry_sample_analyte", + table_name, + ["SamplePtID", "Analyte"], + ) + + +def downgrade() -> None: + """Revert column names and remove added fields.""" + bind = op.get_bind() + inspector = inspect(bind) + if not inspector.has_table("NMA_MinorTraceChemistry"): + return + + table_name = "NMA_MinorTraceChemistry" + columns = _column_names(inspector, table_name) + + unique_constraints = inspector.get_unique_constraints(table_name) + unique_names = {uc.get("name") for uc in unique_constraints} + + if "uq_nma_minor_trace_chemistry_objectid" in unique_names: + op.drop_constraint( + "uq_nma_minor_trace_chemistry_objectid", + table_name, + type_="unique", + ) + + for column_name in ("WCLab_ID", "OBJECTID", "SamplePointID"): + if column_name in columns: + op.drop_column(table_name, column_name) + + rename_map = { + "SamplePtID": "chemistry_sample_info_id", + "Analyte": "analyte", + "SampleValue": "sample_value", + "Units": "units", + "Symbol": "symbol", + "AnalysisMethod": "analysis_method", + "AnalysisDate": "analysis_date", + "Notes": "notes", + "AnalysesAgency": "analyses_agency", + "Uncertainty": "uncertainty", + "Volume": "volume", + "VolumeUnit": "volume_unit", + } + + columns = _column_names(inspector, table_name) + for old_name, new_name in rename_map.items(): + if old_name in columns and new_name not in columns: + op.alter_column(table_name, old_name, new_column_name=new_name) + columns.remove(old_name) + columns.add(new_name) diff --git a/alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py b/alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py new file mode 100644 index 000000000..b31c9fb53 --- /dev/null +++ b/alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py @@ -0,0 +1,27 @@ +"""Merge minor trace alignment and field parameters heads. + +Revision ID: 4f6b7c8d9e0f +Revises: 3a9c1f5b7d2e, c1d2e3f4a5b6 +Create Date: 2026-01-31 12:15:00.000000 +""" + +from typing import Sequence, Union + +# revision identifiers, used by Alembic. +revision: str = "4f6b7c8d9e0f" +down_revision: Union[str, Sequence[str], None] = ( + "3a9c1f5b7d2e", + "c1d2e3f4a5b6", +) +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Merge heads.""" + pass + + +def downgrade() -> None: + """Split heads.""" + pass diff --git a/db/nma_legacy.py b/db/nma_legacy.py index 6f9b4e35f..145080e89 100644 --- a/db/nma_legacy.py +++ b/db/nma_legacy.py @@ -475,7 +475,7 @@ class NMA_MinorTraceChemistry(Base): __tablename__ = "NMA_MinorTraceChemistry" __table_args__ = ( UniqueConstraint( - "sample_pt_id", + "SamplePtID", "Analyte", name="uq_minor_trace_chemistry_sample_analyte", ), @@ -487,6 +487,7 @@ class NMA_MinorTraceChemistry(Base): # FK to ChemistrySampleInfo - required (no orphans) sample_pt_id: Mapped[uuid.UUID] = mapped_column( + "SamplePtID", UUID(as_uuid=True), ForeignKey("NMA_Chemistry_SampleInfo.SamplePtID", ondelete="CASCADE"), nullable=False, diff --git a/tests/integration/test_admin_minor_trace_chemistry.py b/tests/integration/test_admin_minor_trace_chemistry.py index 272256e57..699a83c63 100644 --- a/tests/integration/test_admin_minor_trace_chemistry.py +++ b/tests/integration/test_admin_minor_trace_chemistry.py @@ -84,7 +84,7 @@ def minor_trace_chemistry_record(): # Create MinorTraceChemistry record chemistry = NMA_MinorTraceChemistry( global_id=uuid.uuid4(), - chemistry_sample_info_id=sample_info.sample_pt_id, + sample_pt_id=sample_info.sample_pt_id, analyte="Arsenic", symbol="As", sample_value=0.005, @@ -120,7 +120,7 @@ def test_list_view_contains_view_name(self, admin_client): """List view should contain the view name.""" response = admin_client.get(f"{ADMIN_BASE_URL}/list") assert response.status_code == 200 - assert "Minor Trace Chemistry" in response.text + assert "NMA Minor Trace Chemistry" in response.text def test_no_create_button_in_list_view(self, admin_client): """List view should not have a Create button for read-only view.""" From 8dcfcb76558ac2e7ee66e0c3645ccf510652a7b9 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Tue, 27 Jan 2026 13:08:41 -0700 Subject: [PATCH 09/12] fix: update NMA chemistry lineage tests for sample_pt_id - rename expected FK field from chemistry_sample_info_id to sample_pt_id - include new legacy columns in expected minor trace model fields --- tests/test_nma_chemistry_lineage.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/test_nma_chemistry_lineage.py b/tests/test_nma_chemistry_lineage.py index 3cef600f6..cebe89f8f 100644 --- a/tests/test_nma_chemistry_lineage.py +++ b/tests/test_nma_chemistry_lineage.py @@ -106,12 +106,13 @@ def test_nma_minor_trace_chemistry_columns(): expected_columns = [ "global_id", # PK - "chemistry_sample_info_id", # new FK (UUID, not string) + "sample_pt_id", # FK to NMA_Chemistry_SampleInfo # from legacy + "sample_point_id", "analyte", + "symbol", "sample_value", "units", - "symbol", "analysis_method", "analysis_date", "notes", @@ -119,6 +120,8 @@ def test_nma_minor_trace_chemistry_columns(): "uncertainty", "volume", "volume_unit", + "object_id", + "wclab_id", ] for col in expected_columns: @@ -164,7 +167,7 @@ def test_nma_minor_trace_chemistry_save_all_columns(shared_well): # Verify all columns saved assert mtc.global_id is not None - assert mtc.chemistry_sample_info_id == sample_info.sample_pt_id + assert mtc.sample_pt_id == sample_info.sample_pt_id assert mtc.analyte == "As" assert mtc.sample_value == 0.015 assert mtc.units == "mg/L" @@ -384,7 +387,7 @@ def test_append_mtc_to_sample_info(shared_well): # Verify bidirectional assert mtc.chemistry_sample_info == sample_info - assert mtc.chemistry_sample_info_id == sample_info.sample_pt_id + assert mtc.sample_pt_id == sample_info.sample_pt_id session.delete(sample_info) session.commit() @@ -410,7 +413,7 @@ def test_mtc_requires_chemistry_sample_info(): analyte="As", sample_value=0.01, units="mg/L", - chemistry_sample_info_id=None, # Explicit None triggers validator + sample_pt_id=None, # Explicit None triggers validator ) @@ -528,7 +531,7 @@ def test_cascade_delete_sample_info_deletes_mtc(shared_well): sample_info_id = sample_info.sample_pt_id assert ( session.query(NMA_MinorTraceChemistry) - .filter_by(chemistry_sample_info_id=sample_info_id) + .filter_by(sample_pt_id=sample_info_id) .count() == 4 ) @@ -540,7 +543,7 @@ def test_cascade_delete_sample_info_deletes_mtc(shared_well): # Children should be gone assert ( session.query(NMA_MinorTraceChemistry) - .filter_by(chemistry_sample_info_id=sample_info_id) + .filter_by(sample_pt_id=sample_info_id) .count() == 0 ) From d7c93ec03654b54b4cfbedc0f82108048a3d8e82 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Tue, 27 Jan 2026 13:15:30 -0700 Subject: [PATCH 10/12] test: update admin minor trace chemistry feature for sample_pt_id - replace chemistry_sample_info_id with sample_pt_id in the admin feature field list - why: model/admin config now exposes the legacy SamplePtID mapping, so the feature spec must align with the current schema naming --- tests/features/admin-minor-trace-chemistry.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/features/admin-minor-trace-chemistry.feature b/tests/features/admin-minor-trace-chemistry.feature index 1d09b8e40..b8c035b5c 100644 --- a/tests/features/admin-minor-trace-chemistry.feature +++ b/tests/features/admin-minor-trace-chemistry.feature @@ -31,7 +31,7 @@ Feature: Minor Trace Chemistry Admin View Then the Minor Trace Chemistry admin view should have these fields configured: | field | | global_id | - | chemistry_sample_info_id | + | sample_pt_id | | analyte | | symbol | | sample_value | From 99e2e1d3040069d6eacf94bb021167e7a1b590c8 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Wed, 28 Jan 2026 13:26:21 -0700 Subject: [PATCH 11/12] refactor: Rebase NMA minor trace migration onto new base - Update 3a9c1f5b7d2e to point at c1d2e3f4a5b6 - Remove the obsolete merge revision 4f6b7c8d9e0f - Reason: manual rebase to realign the migration chain after history changes and avoid a redundant merge node --- ...c1f5b7d2e_align_nma_minor_trace_columns.py | 4 +-- ..._merge_minor_trace_and_field_parameters.py | 27 ------------------- 2 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py diff --git a/alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py b/alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py index 0b625144e..b2ceb077e 100644 --- a/alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py +++ b/alembic/versions/3a9c1f5b7d2e_align_nma_minor_trace_columns.py @@ -1,7 +1,7 @@ """Align NMA_MinorTraceChemistry columns with legacy schema. Revision ID: 3a9c1f5b7d2e -Revises: 2d67da5ff3ae +Revises: c1d2e3f4a5b6 Create Date: 2026-01-31 12:00:00.000000 """ @@ -13,7 +13,7 @@ # revision identifiers, used by Alembic. revision: str = "3a9c1f5b7d2e" -down_revision: Union[str, Sequence[str], None] = "2d67da5ff3ae" +down_revision: Union[str, Sequence[str], None] = "c1d2e3f4a5b6" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py b/alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py deleted file mode 100644 index b31c9fb53..000000000 --- a/alembic/versions/4f6b7c8d9e0f_merge_minor_trace_and_field_parameters.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Merge minor trace alignment and field parameters heads. - -Revision ID: 4f6b7c8d9e0f -Revises: 3a9c1f5b7d2e, c1d2e3f4a5b6 -Create Date: 2026-01-31 12:15:00.000000 -""" - -from typing import Sequence, Union - -# revision identifiers, used by Alembic. -revision: str = "4f6b7c8d9e0f" -down_revision: Union[str, Sequence[str], None] = ( - "3a9c1f5b7d2e", - "c1d2e3f4a5b6", -) -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Merge heads.""" - pass - - -def downgrade() -> None: - """Split heads.""" - pass From d5db96defc2e9ee22877d73c5360e0bf2e84f05c Mon Sep 17 00:00:00 2001 From: Chase Martin Date: Wed, 28 Jan 2026 17:10:38 -0800 Subject: [PATCH 12/12] fix: update field names in transfer scripts --- transfers/minor_trace_chemistry_transfer.py | 49 +++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/transfers/minor_trace_chemistry_transfer.py b/transfers/minor_trace_chemistry_transfer.py index ee9c314e8..60ade7560 100644 --- a/transfers/minor_trace_chemistry_transfer.py +++ b/transfers/minor_trace_chemistry_transfer.py @@ -129,16 +129,16 @@ def _transfer_hook(self, session: Session) -> None: stmt = insert_stmt.values(chunk).on_conflict_do_update( index_elements=["GlobalID"], set_={ - "sample_value": excluded.sample_value, - "units": excluded.units, - "symbol": excluded.symbol, - "analysis_method": excluded.analysis_method, - "analysis_date": excluded.analysis_date, - "notes": excluded.notes, - "analyses_agency": excluded.analyses_agency, - "uncertainty": excluded.uncertainty, - "volume": excluded.volume, - "volume_unit": excluded.volume_unit, + "SampleValue": excluded.SampleValue, + "Units": excluded.Units, + "Symbol": excluded.Symbol, + "AnalysisMethod": excluded.AnalysisMethod, + "AnalysisDate": excluded.AnalysisDate, + "Notes": excluded.Notes, + "AnalysesAgency": excluded.AnalysesAgency, + "Uncertainty": excluded.Uncertainty, + "Volume": excluded.Volume, + "VolumeUnit": excluded.VolumeUnit, }, ) session.execute(stmt) @@ -174,26 +174,27 @@ def _row_to_dict(self, row) -> Optional[dict[str, Any]]: return None return { - "global_id": global_id, - "chemistry_sample_info_id": sample_pt_id, - "analyte": self._safe_str(row, "Analyte"), - "sample_value": self._safe_float(row, "SampleValue"), - "units": self._safe_str(row, "Units"), - "symbol": self._safe_str(row, "Symbol"), - "analysis_method": self._safe_str(row, "AnalysisMethod"), - "analysis_date": self._parse_date(row, "AnalysisDate"), - "notes": self._safe_str(row, "Notes"), - "analyses_agency": self._safe_str(row, "AnalysesAgency"), - "uncertainty": self._safe_float(row, "Uncertainty"), - "volume": self._safe_int(row, "Volume"), - "volume_unit": self._safe_str(row, "VolumeUnit"), + "GlobalID": global_id, + "SamplePtID": sample_pt_id, + "SamplePointID": self._safe_str(row, "SamplePointID"), + "Analyte": self._safe_str(row, "Analyte"), + "SampleValue": self._safe_float(row, "SampleValue"), + "Units": self._safe_str(row, "Units"), + "Symbol": self._safe_str(row, "Symbol"), + "AnalysisMethod": self._safe_str(row, "AnalysisMethod"), + "AnalysisDate": self._parse_date(row, "AnalysisDate"), + "Notes": self._safe_str(row, "Notes"), + "AnalysesAgency": self._safe_str(row, "AnalysesAgency"), + "Uncertainty": self._safe_float(row, "Uncertainty"), + "Volume": self._safe_int(row, "Volume"), + "VolumeUnit": self._safe_str(row, "VolumeUnit"), } def _dedupe_rows(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: """Dedupe rows by unique key to avoid ON CONFLICT loops. Later rows win.""" deduped = {} for row in rows: - key = row.get("global_id") + key = row.get("GlobalID") if key is None: continue deduped[key] = row