diff --git a/luminex/src/org/labkey/luminex/query/AnalyteSinglePointControlTable.java b/luminex/src/org/labkey/luminex/query/AnalyteSinglePointControlTable.java index 0b4fd82e7..4c3095c79 100644 --- a/luminex/src/org/labkey/luminex/query/AnalyteSinglePointControlTable.java +++ b/luminex/src/org/labkey/luminex/query/AnalyteSinglePointControlTable.java @@ -28,7 +28,6 @@ import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.Table; import org.labkey.api.data.TableSelector; -import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.query.ExprColumn; import org.labkey.api.query.FieldKey; import org.labkey.api.query.InvalidKeyException; @@ -81,18 +80,13 @@ public AnalyteSinglePointControlTable(final LuminexProtocolSchema schema, Contai SQLFragment avgFiSQL = new SQLFragment("(SELECT AVG(dr.FIBackground) FROM ("); // TODO ContainerFilter -- Do we really want a non-permission checking container filter here? LuminexDataTable dataTable = schema.createDataTable(ContainerFilter.getUnsafeEverythingFilter(), false); - List dataColumns = Arrays.asList(dataTable.getColumn("FlaggedAsExcluded"), dataTable.getColumn("FIBackground"), dataTable.getColumn("Description"), dataTable.getColumn("Data"), dataTable.getColumn("Analyte")); + List dataColumns = Arrays.asList(dataTable.getColumn("FlaggedAsExcluded"), dataTable.getColumn("FIBackground"), dataTable.getColumn("SinglePointControl"), dataTable.getColumn("Analyte")); avgFiSQL.append(QueryService.get().getSelectBuilder(dataTable).columns(dataColumns).buildSqlFragment()); - avgFiSQL.append(") dr, "); - avgFiSQL.append(ExperimentService.get().getTinfoData(), "d"); - avgFiSQL.append(", "); - avgFiSQL.append(LuminexProtocolSchema.getTableInfoSinglePointControl(), "spc"); - avgFiSQL.append(" WHERE dr.Description = spc.Name AND dr.Data = d.RowId AND d.RunId = spc.RunId "); - avgFiSQL.append(" AND dr.Analyte = "); + avgFiSQL.append(") dr WHERE dr.SinglePointControl = "); avgFiSQL.append(ExprColumn.STR_TABLE_ALIAS); - avgFiSQL.append(".AnalyteId AND spc.RowId = "); + avgFiSQL.append(".SinglePointControlId AND dr.Analyte = "); avgFiSQL.append(ExprColumn.STR_TABLE_ALIAS); - avgFiSQL.append(".SinglePointControlId AND dr.FlaggedAsExcluded = ?)"); + avgFiSQL.append(".AnalyteId AND dr.FlaggedAsExcluded = ?)"); avgFiSQL.add(false); ExprColumn avgFiColumn = new ExprColumn(this, "AverageFiBkgd", avgFiSQL, JdbcType.DOUBLE); diff --git a/luminex/test/src/org/labkey/test/tests/luminex/LuminexSinglePointTest.java b/luminex/test/src/org/labkey/test/tests/luminex/LuminexSinglePointTest.java index b339f9614..09336cda7 100644 --- a/luminex/test/src/org/labkey/test/tests/luminex/LuminexSinglePointTest.java +++ b/luminex/test/src/org/labkey/test/tests/luminex/LuminexSinglePointTest.java @@ -18,11 +18,17 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; +import org.labkey.api.query.QueryKey; +import org.labkey.remoteapi.CommandResponse; import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.assay.GetProtocolCommand; import org.labkey.remoteapi.assay.Protocol; import org.labkey.remoteapi.assay.ProtocolResponse; import org.labkey.remoteapi.assay.SaveProtocolCommand; +import org.labkey.remoteapi.query.Filter; +import org.labkey.remoteapi.query.Row; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.SortDirection; @@ -32,11 +38,14 @@ import org.labkey.test.components.html.BootstrapMenu; import org.labkey.test.pages.ReactAssayDesignerPage; import org.labkey.test.pages.luminex.LeveyJenningsPlotWindow; +import org.labkey.test.util.APIAssayHelper; import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.luminex.LuminexGuideSetHelper; +import org.labkey.test.util.luminex.LuminexSaveExclusionCommand; import java.util.Arrays; import java.util.Calendar; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -92,7 +101,7 @@ public void testAssayOverAPI() throws Exception } @Test - public void testSinglePoint() + public void testSinglePoint() throws Exception { importRun(file1, 1); importRun(file2, 2); @@ -146,6 +155,43 @@ public void testSinglePoint() assertTextNotPresent(file4); verifyValueBasedGuideSet(); + + verifyWellExclusionRemovedFromAverage(); + } + + private void verifyWellExclusionRemovedFromAverage() throws Exception + { + String controlName = "IH5672"; + String analyteName = "ENV1"; + String excludedWell = "H11"; + + log("Exclude well " + excludedWell + " of single point control " + controlName + " for " + analyteName + " in " + file1); + Connection connection = createDefaultConnection(); + SelectRowsCommand select = new SelectRowsCommand("assay.Luminex." + QueryKey.encodePart(TEST_ASSAY_LUM), "Data"); + select.setColumns(List.of("Data", "Analyte", "Type", "SinglePointControl/Run/RowId")); + select.addFilter(new Filter("Data/Name", file1, Filter.Operator.EQUAL)); + select.addFilter(new Filter("Description", controlName, Filter.Operator.EQUAL)); + select.addFilter(new Filter("Analyte/Name", analyteName, Filter.Operator.EQUAL)); + select.addFilter(new Filter("Well", excludedWell, Filter.Operator.EQUAL)); + SelectRowsResponse selectResponse = select.execute(connection, getProjectName()); + assertEquals("Expected one data row for the well to exclude", 1, selectResponse.getRowCount().intValue()); + Row well = selectResponse.getRowset().iterator().next(); + + int assayId = new APIAssayHelper(this).getIdFromAssayName(TEST_ASSAY_LUM, getProjectName()); + CommandResponse exclusionResponse = new LuminexSaveExclusionCommand(assayId, ((Number) well.getValue("SinglePointControl/Run/RowId")).longValue()) + .addWellExclusion(((Number) well.getValue("Data")).longValue(), controlName, (String) well.getValue("Type"), + excludedWell, ((Number) well.getValue("Analyte")).longValue(), "Exclude one single point control well") + .execute(connection, getProjectName()); + beginAt((String) exclusionResponse.getParsedData().get("returnUrl")); + waitForPipelineJobsToComplete(6, false); + + goToTestAssayHome(); + BootstrapMenu.find(getDriver(), "view qc report").clickSubMenu(true, "view single point control qc report"); + DataRegionTable tbl = new DataRegionTable("AnalyteSinglePointControl", getDriver()); + tbl.setFilter("Analyte", "Equals", analyteName); + tbl.setSort("SinglePointControl/Run/Name", SortDirection.ASC); + assertEquals("Excluded well should be dropped from the average", "25.0", tbl.getDataAsText(0, "Average Fi Bkgd")); + assertEquals("Run without exclusions should be unchanged", "30.0", tbl.getDataAsText(1, "Average Fi Bkgd")); } private void verifyValueBasedGuideSet() diff --git a/luminex/test/src/org/labkey/test/util/luminex/LuminexSaveExclusionCommand.java b/luminex/test/src/org/labkey/test/util/luminex/LuminexSaveExclusionCommand.java new file mode 100644 index 000000000..8ed70ff87 --- /dev/null +++ b/luminex/test/src/org/labkey/test/util/luminex/LuminexSaveExclusionCommand.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.util.luminex; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.labkey.remoteapi.CommandResponse; +import org.labkey.remoteapi.PostCommand; + +/** + * Wrapper for luminex-saveExclusion.api, which queues a pipeline job to apply the exclusion commands. + */ +public class LuminexSaveExclusionCommand extends PostCommand +{ + private final int _assayId; + private final long _runId; + private final JSONArray _commands = new JSONArray(); + + public LuminexSaveExclusionCommand(int assayId, long runId) + { + super("luminex", "saveExclusion"); + _assayId = assayId; + _runId = runId; + } + + public LuminexSaveExclusionCommand addWellExclusion(long dataId, String description, String type, String well, long analyteRowId, String comment) + { + JSONObject command = new JSONObject(); + command.put("command", "insert"); + command.put("dataId", dataId); + command.put("description", description); + command.put("type", type); + command.put("well", well); + command.put("analyteRowIds", String.valueOf(analyteRowId)); + command.put("comment", comment); + _commands.put(command); + return this; + } + + @Override + public JSONObject getJsonObject() + { + JSONObject json = new JSONObject(); + json.put("assayId", _assayId); + json.put("tableName", "WellExclusion"); + json.put("runId", _runId); + json.put("commands", _commands); + return json; + } +}