diff --git a/snprc_ehr/build.gradle b/snprc_ehr/build.gradle index bb7bfee92..12c4d3087 100644 --- a/snprc_ehr/build.gradle +++ b/snprc_ehr/build.gradle @@ -3,6 +3,7 @@ import org.labkey.gradle.util.ExternalDependency dependencies { + BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:platform:study", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:ehrModules:ehr", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:snprcEHRModules:snd", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile") diff --git a/snprc_ehr/resources/queries/study/GenDemoCustomizer.sql b/snprc_ehr/resources/queries/study/GenDemoCustomizer.sql index bb9c6979d..04fc0d3f2 100644 --- a/snprc_ehr/resources/queries/study/GenDemoCustomizer.sql +++ b/snprc_ehr/resources/queries/study/GenDemoCustomizer.sql @@ -16,10 +16,13 @@ Select d.Id, d.species, d.species.arc_species_code as ARC_species, */ - IFNULL(g.HasGeneExpressionData,0) as HasGeneExpressionData, - IFNULL(s.HasSNPData,0) as HasSNPData, - IFNULL(m.HasMicrosatellitesData,0) as HasMicrosatellitesData, - IFNULL(p.HasphenotypesData,0) as HasPhenotypeData + -- Cast boolean → integer first so the IFNULL result type is INTEGER, not BOOLEAN. + -- Both args matching as BOOLEAN triggers SQL Server's BIT→BOOLEAN CASE-wrap (error 4145); + -- mixing BOOLEAN with integer 0 fails on Postgres ("COALESCE types boolean and integer cannot be matched"). + IFNULL(CAST(g.HasGeneExpressionData AS INTEGER), 0) as HasGeneExpressionData, + IFNULL(CAST(s.HasSNPData AS INTEGER), 0) as HasSNPData, + IFNULL(CAST(m.HasMicrosatellitesData AS INTEGER), 0) as HasMicrosatellitesData, + IFNULL(CAST(p.HasphenotypesData AS INTEGER), 0) as HasPhenotypeData From study.demographics d LEFT OUTER JOIN study.GenFlagSNP s diff --git a/snprc_ehr/resources/queries/study/GenDemoHasData.sql b/snprc_ehr/resources/queries/study/GenDemoHasData.sql index ccbf954af..75ff62f44 100644 --- a/snprc_ehr/resources/queries/study/GenDemoHasData.sql +++ b/snprc_ehr/resources/queries/study/GenDemoHasData.sql @@ -7,10 +7,10 @@ Select d.Id, d.gender, d.species, d.species.arc_species_code as ARC_species, - IFNULL(g.HasGeneExpressionData,0) as HasGeneExpressionData, - IFNULL(s.HasSNPData,0) as HasSNPData, - IFNULL(m.HasMicrosatellitesData,0) as HasMicrosatellitesData, - IFNULL(p.HasphenotypesData,0) as HasPhenotypeData + IFNULL(CAST(g.HasGeneExpressionData AS INTEGER), 0) as HasGeneExpressionData, + IFNULL(CAST(s.HasSNPData AS INTEGER), 0) as HasSNPData, + IFNULL(CAST(m.HasMicrosatellitesData AS INTEGER), 0) as HasMicrosatellitesData, + IFNULL(CAST(p.HasphenotypesData AS INTEGER), 0) as HasPhenotypeData From study.demographics d LEFT OUTER JOIN study.GenFlagSNP s diff --git a/snprc_ehr/resources/queries/study/GenHasMicrosatellites.query.xml b/snprc_ehr/resources/queries/study/GenHasMicrosatellites.query.xml index ca31f55e4..d0c10c07a 100644 --- a/snprc_ehr/resources/queries/study/GenHasMicrosatellites.query.xml +++ b/snprc_ehr/resources/queries/study/GenHasMicrosatellites.query.xml @@ -11,7 +11,6 @@ animal Id - diff --git a/snprc_ehr/resources/queries/study/GenHasPhenotype.query.xml b/snprc_ehr/resources/queries/study/GenHasPhenotype.query.xml index 34bdcf847..4214f751b 100644 --- a/snprc_ehr/resources/queries/study/GenHasPhenotype.query.xml +++ b/snprc_ehr/resources/queries/study/GenHasPhenotype.query.xml @@ -11,7 +11,6 @@ animal Id - diff --git a/snprc_ehr/resources/queries/study/GenHasSNP.query.xml b/snprc_ehr/resources/queries/study/GenHasSNP.query.xml index 4abc4519b..d572850ec 100644 --- a/snprc_ehr/resources/queries/study/GenHasSNP.query.xml +++ b/snprc_ehr/resources/queries/study/GenHasSNP.query.xml @@ -11,7 +11,6 @@ animal Id - diff --git a/snprc_ehr/resources/queries/study/LabworkOvaParasite.sql b/snprc_ehr/resources/queries/study/LabworkOvaParasite.sql deleted file mode 100644 index a4f3f0c87..000000000 --- a/snprc_ehr/resources/queries/study/LabworkOvaParasite.sql +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2019 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. - */ -SELECT ServiceId.Dataset, ServiceId.ServiceName, * -FROM snprc_ehr.labwork_panels -WHERE ServiceId.Dataset='Parasitology' and ServiceId.ServiceName in ('OVA & PARASITES' , 'OVA & PARASITES, URINE' ) -SELECT - l.id, - l.date, - l.project, - l.testid, - l.resultOORIndicator, - l.value_type, - lt.testName, - l.result, - l.qualresult, - l.units, - l.refRange, - l.abnormal_flags, - l.remark, - l.description, - l.runid, - l.taskid, - l.method, - l.objectid, - l.modified, - l.modifiedby, - l.created, - l.createdby -FROM labworkResults as l - inner join snprc_ehr.labwork_panels as lt - on l.serviceTestid = lt.rowId and lt.TestId in (77, 201, 727) \ No newline at end of file diff --git a/snprc_ehr/resources/queries/study/ReportTcruziNewPositives.query.xml b/snprc_ehr/resources/queries/study/ReportTcruziNewPositives.query.xml index 771b551b4..4b9306e69 100644 --- a/snprc_ehr/resources/queries/study/ReportTcruziNewPositives.query.xml +++ b/snprc_ehr/resources/queries/study/ReportTcruziNewPositives.query.xml @@ -27,9 +27,6 @@ First Date - - - diff --git a/snprc_ehr/resources/queries/study/ReportTcruziSummaryAll.query.xml b/snprc_ehr/resources/queries/study/ReportTcruziSummaryAll.query.xml index d42e2b85f..4c85c4717 100644 --- a/snprc_ehr/resources/queries/study/ReportTcruziSummaryAll.query.xml +++ b/snprc_ehr/resources/queries/study/ReportTcruziSummaryAll.query.xml @@ -31,9 +31,6 @@ Test Count - - - diff --git a/snprc_ehr/resources/queries/study/chemMisc.sql b/snprc_ehr/resources/queries/study/chemMisc.sql index 6833c8381..6b1e228dd 100644 --- a/snprc_ehr/resources/queries/study/chemMisc.sql +++ b/snprc_ehr/resources/queries/study/chemMisc.sql @@ -7,7 +7,7 @@ SELECT b.id, b.date, b.testId, - b.testId.Name, + lt.testName, b.resultOORIndicator, b.result, @@ -17,6 +17,9 @@ SELECT b.taskid, b.runId FROM study.labworkResults b - -WHERE b.testId.Type = 'Biochemistry' AND (b.testId.includeInPanel = false OR b.testId.includeInPanel IS NULL) AND b.qcstate.publicdata = true +INNER JOIN snprc_ehr.labwork_panels AS lt + ON b.serviceTestid = lt.rowId + AND lt.ServiceId.Dataset='Biochemistry' + AND (b.serviceTestid.includeInPanel = false OR b.serviceTestid.includeInPanel IS NULL) + AND b.qcstate.publicdata = true diff --git a/snprc_ehr/resources/queries/study/colonyPopulationChange.sql b/snprc_ehr/resources/queries/study/colonyPopulationChange.sql new file mode 100644 index 000000000..7744967b3 --- /dev/null +++ b/snprc_ehr/resources/queries/study/colonyPopulationChange.sql @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ +PARAMETERS(StartDate TIMESTAMP, EndDate TIMESTAMP) + +SELECT + T1.id, + T1.id.dataset.demographics.species as species, + 'Births' AS Category, + T1.date, + convert(year(T1.date), integer) AS Year + +FROM study.Birth T1 +WHERE T1.date IS NOT NULL +and cast(COALESCE(STARTDATE, '1900-01-01') as date) <= T1.date and cast(COALESCE(ENDDATE, curdate()) as date) >= cast(T1.date as date) + +UNION ALL + +SELECT + T2.id, + T2.id.dataset.demographics.species, + 'Arrivals' AS Category, + T2.date, + convert(year(T2.date), INTEGER) AS Year + +FROM study.Arrival T2 +WHERE T2.date IS NOT NULL +AND T2.qcstate.publicdata = true +and cast(COALESCE(STARTDATE, '1900-01-01') as date) <= T2.date and cast(COALESCE(ENDDATE, curdate()) as date) >= cast(T2.date as date) + +UNION ALL + +SELECT + T3.id, + T3.id.dataset.demographics.species, + 'Departures' AS Category, + T3.date, + convert(year(T3.date), INTEGER) AS Year + +FROM study.Departure T3 +WHERE T3.date IS NOT NULL +AND T3.qcstate.publicdata = true +and cast(COALESCE(STARTDATE, '1900-01-01') as date) <= T3.date and cast(COALESCE(ENDDATE, curdate()) as date) >= cast(T3.date as date) + +UNION ALL + +SELECT + T4.id, + T4.id.dataset.demographics.species, + 'Deaths' AS Category, + T4.date, + convert(year(T4.date), INTEGER) AS Year + +FROM study.Deaths T4 +WHERE T4.date IS NOT NULL +and cast(COALESCE(STARTDATE, '1900-01-01') as date) <= T4.date and cast(COALESCE(ENDDATE, curdate()) as date) >= cast(T4.date as date) diff --git a/snprc_ehr/resources/queries/study/hematologyMisc.sql b/snprc_ehr/resources/queries/study/hematologyMisc.sql index c9f8fa4d9..81c4c9e07 100644 --- a/snprc_ehr/resources/queries/study/hematologyMisc.sql +++ b/snprc_ehr/resources/queries/study/hematologyMisc.sql @@ -15,5 +15,8 @@ SELECT b.taskid, b.runId FROM study.labworkResults b - -WHERE (testId.includeInPanel = FALSE OR b.testid.includeInPanel IS NULL) and b.qcstate.publicdata = true + INNER JOIN snprc_ehr.labwork_panels AS lt + ON b.serviceTestid = lt.rowId + AND lt.ServiceId.Dataset='Hematology' + AND (b.serviceTestid.includeInPanel = false OR b.serviceTestid.includeInPanel IS NULL) + AND b.qcstate.publicdata = true diff --git a/snprc_ehr/resources/queries/study/labworkResultsAll.query.xml b/snprc_ehr/resources/queries/study/labworkResultsAll.query.xml index ade016a6a..9168b0ca5 100644 --- a/snprc_ehr/resources/queries/study/labworkResultsAll.query.xml +++ b/snprc_ehr/resources/queries/study/labworkResultsAll.query.xml @@ -107,7 +107,7 @@ UserId Users core - ` + true diff --git a/snprc_ehr/resources/schemas/dbscripts/sqlserver/snprc_ehr-26.000-26.001.sql b/snprc_ehr/resources/schemas/dbscripts/sqlserver/snprc_ehr-26.000-26.001.sql new file mode 100644 index 000000000..143a3a56e --- /dev/null +++ b/snprc_ehr/resources/schemas/dbscripts/sqlserver/snprc_ehr-26.000-26.001.sql @@ -0,0 +1,2 @@ +EXEC core.executeJavaUpgradeCode 'fixSpecimenStorageColumnNames'; + diff --git a/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java index 7f22b236a..d566d66c4 100644 --- a/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java +++ b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRModule.java @@ -21,6 +21,7 @@ import org.labkey.api.data.Container; import org.labkey.api.data.DbSchema; import org.labkey.api.data.ObjectFactory; +import org.labkey.api.data.UpgradeCode; import org.labkey.api.ehr.EHRService; import org.labkey.api.ehr.dataentry.DefaultDataEntryFormFactory; import org.labkey.api.ehr.history.DefaultArrivalDataSource; @@ -119,7 +120,14 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 26.000; + return 26.001; + } + + @Nullable + @Override + public UpgradeCode getUpgradeCode() + { + return new SNPRC_EHRUpgradeCode(); } @Override diff --git a/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRUpgradeCode.java b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRUpgradeCode.java new file mode 100644 index 000000000..dccefd73c --- /dev/null +++ b/snprc_ehr/src/org/labkey/snprc_ehr/SNPRC_EHRUpgradeCode.java @@ -0,0 +1,132 @@ +/* + * 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.snprc_ehr; + +import org.apache.logging.log4j.Logger; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbSchemaType; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.DbScope.Transaction; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SchemaTableInfo; +import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.Table; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.UpgradeCode; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.module.ModuleContext; +import org.labkey.api.specimen.model.SpecimenTablesProvider; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.logging.LogHelper; + +import java.util.List; + +public class SNPRC_EHRUpgradeCode implements UpgradeCode +{ + private static final Logger LOG = LogHelper.getLogger(SNPRC_EHRUpgradeCode.class, "SNPRC_EHR upgrade status"); + + /** + * Called from snprc_ehr-26.000-26.001.sql + * + * For every PropertyDescriptor in the specimen storage schema, verify that its StorageColumnName + * exactly matches (case-sensitively) the physical metadata identifier of a column on the provisioned + * table. If not, rewrite it to the physical identifier of the column resolved either by the existing + * (case-insensitive) StorageColumnName lookup or, failing that, by the descriptor's Name. This makes + * the value still match after migration to a case-sensitive database. + */ + @SuppressWarnings("unused") + public static void fixSpecimenStorageColumnNames(ModuleContext context) + { + if (context.isNewInstall()) + return; + + TableInfo tinfoDomainDescriptor = OntologyManager.getTinfoDomainDescriptor(); + TableInfo tinfoPropertyDomain = OntologyManager.getTinfoPropertyDomain(); + TableInfo tinfoPropertyDescriptor = OntologyManager.getTinfoPropertyDescriptor(); + + DbScope scope = tinfoPropertyDescriptor.getSchema().getScope(); + DbSchema specimenSchema = DbSchema.get(SpecimenTablesProvider.SCHEMA_NAME, DbSchemaType.Provisioned); + + SQLFragment sql = new SQLFragment("SELECT px.PropertyId, dd.StorageSchemaName, dd.StorageTableName, px.StorageColumnName, px.Name FROM ") + .append(tinfoDomainDescriptor, "dd") + .append(" INNER JOIN ") + .append(tinfoPropertyDomain, "pd") + .append(" ON dd.DomainId = pd.DomainId INNER JOIN ") + .append(tinfoPropertyDescriptor, "px") + .append(" ON pd.PropertyId = px.PropertyId ") + .append("WHERE dd.StorageSchemaName = ?").add(SpecimenTablesProvider.SCHEMA_NAME) + .append(" AND dd.StorageTableName IS NOT NULL AND px.StorageColumnName IS NOT NULL"); + + List properties = new SqlSelector(scope, sql).getArrayList(PropertyRow.class); + LOG.info("Checking {} PropertyDescriptors in specimen storage schema '{}'", properties.size(), SpecimenTablesProvider.SCHEMA_NAME); + + int updated = 0; + int skipped = 0; + int alreadyCorrect = 0; + try (Transaction tx = scope.ensureTransaction()) + { + for (PropertyRow row : properties) + { + SchemaTableInfo provisioned = specimenSchema.getTable(row.storageTableName()); + if (provisioned == null) + { + LOG.warn("Provisioned table '{}.{}' does not exist; skipping property '{}'", + row.storageSchemaName(), row.storageTableName(), row.name()); + skipped++; + continue; + } + + // getColumn() is case-insensitive, so a case-mismatched StorageColumnName still resolves + // on SQL Server but will break after migration to a case-sensitive DB. Compare to the + // physical metadata identifier to decide whether a rewrite is needed. + ColumnInfo byStorage = provisioned.getColumn(row.storageColumnName()); + if (byStorage != null && row.storageColumnName().equals(byStorage.getMetaDataIdentifier().getId())) + { + LOG.debug("Property '{}' in '{}.{}': StorageColumnName '{}' already matches physical column case; nothing to update", + row.name(), row.storageSchemaName(), row.storageTableName(), row.storageColumnName()); + alreadyCorrect++; + continue; + } + + ColumnInfo target = byStorage != null ? byStorage : provisioned.getColumn(row.name()); + if (target == null) + { + LOG.warn("Property '{}' in '{}.{}': neither StorageColumnName '{}' nor Name '{}' exists in provisioned table; skipping", + row.name(), row.storageSchemaName(), row.storageTableName(), row.storageColumnName(), row.name()); + skipped++; + continue; + } + + String newStorageColumnName = target.getMetaDataIdentifier().getId(); + LOG.info("Updating StorageColumnName from '{}' to '{}' for property '{}' in table '{}.{}'", + row.storageColumnName(), newStorageColumnName, row.name(), row.storageSchemaName(), row.storageTableName()); + Table.update(null, tinfoPropertyDescriptor, PageFlowUtil.map("StorageColumnName", newStorageColumnName), row.propertyId()); + updated++; + } + tx.commit(); + } + + // Drop any cached PropertyDescriptors that may have been populated earlier in startup with the + // pre-update StorageColumnName. + OntologyManager.clearCaches(); + + LOG.info("Specimen StorageColumnName fix complete: {} updated, {} skipped, {} already correct", + updated, skipped, alreadyCorrect); + } + + public record PropertyRow(int propertyId, String storageSchemaName, String storageTableName, String storageColumnName, String name) {} +} diff --git a/snprc_ehr/test/src/org/labkey/test/tests/snprc_ehr/SNPRC_EHRTest.java b/snprc_ehr/test/src/org/labkey/test/tests/snprc_ehr/SNPRC_EHRTest.java index 5b106e7c3..9c881d5c6 100644 --- a/snprc_ehr/test/src/org/labkey/test/tests/snprc_ehr/SNPRC_EHRTest.java +++ b/snprc_ehr/test/src/org/labkey/test/tests/snprc_ehr/SNPRC_EHRTest.java @@ -26,6 +26,8 @@ import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.query.InsertRowsCommand; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.remoteapi.query.TruncateTableCommand; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; @@ -40,6 +42,7 @@ import org.labkey.test.components.ext4.widgets.SearchPanel; import org.labkey.test.pages.ehr.AnimalHistoryPage; import org.labkey.test.pages.ehr.ColonyOverviewPage; +import org.labkey.test.pages.ehr.EHRAdminPage; import org.labkey.test.pages.ehr.ParticipantViewPage; import org.labkey.test.pages.snprc_ehr.SNPRCAnimalHistoryPage; import org.labkey.test.tests.ehr.AbstractGenericEHRTest; @@ -100,6 +103,16 @@ public class SNPRC_EHRTest extends AbstractGenericEHRTest implements SqlserverOn private static final String ANIMAL_HISTORY_URL = "/" + PROJECT_NAME + "/ehr-animalHistory.view"; private static final String SNPRC_ROOM_ID = "S824778"; private static final String SNPRC_ROOM_ID2 = "S043365"; + private static final List SND_CATEGORIES = Arrays.asList( + "Alopecia", + "BCS", + "Cumulative Blood", + "Vitals Temperature", + "TB", + "Vitals", + "Weight"); + private static final int SND_PKG_ID_START = 901; + private static final int SND_SUPER_PKG_ID_START = 1900; private static int _pipelineJobCount = 0; @@ -168,29 +181,87 @@ private void doSetup() throws Exception new RReportHelper(this).ensureRConfig(); initProject("SNPRC EHR"); goToProjectHome(); - _containerHelper.enableModules(Arrays.asList("SND")); createTestSubjects(); initGenetics(); - initSND(); + completeSnprcPostStudyImportSetup(); + } + + @Override + protected void initCreatedProject() throws Exception + { + setFormatStrings(); + setEHRModuleProperties(); + createUsersandPermissions(); + + doExtraPreStudyImportSetup(); + + defineQCStates(); + populateInitialData(); + + prepareSnprcPreStudyImportSetup(); + + goToProjectHome(); + + // Populate hard tables (e.g. snprc_ehr.animal_groups) before importing the study so that + // saved queries that pivot over those tables resolve their dynamic columns during import-time + // query validation. Otherwise queries like baboonAssignedColonyUsage / colonyUsageQuery fail + // with "Unknown field [col.pc_SPF::colonytotal]" because the pivot subquery returns no rows. + populateHardTableRecords(); + + importStudy(); + disableMiniProfiler(); + defineQCStates(); + setupStudyPermissions(); + primeCaches(); + } + + private void prepareSnprcPreStudyImportSetup() throws Exception + { goToProjectHome(); + _containerHelper.enableModules(Arrays.asList("SND")); + initSND(); + createSNDCategories(); + createSNDPackages(); + + addExtensibleCols(); + clickFolder(GENETICSFOLDER); _assayHelper = new APIAssayHelper(this); _assayHelper.uploadXarFileAsAssayDesign(ASSAY_GENE_EXPRESSION_XAR, 1); _assayHelper.uploadXarFileAsAssayDesign(ASSAY_MICROSATELLITES_XAR, 2); _assayHelper.uploadXarFileAsAssayDesign(ASSAY_PHENOTYPES_XAR, 3); _assayHelper.uploadXarFileAsAssayDesign(ASSAY_SNPS_XAR, 4); + + _assayHelper.importAssay(ASSAY_GENE_EXPRESSION, ASSAY_GENE_EXPRESSION_TSV, getGeneticsPath()); + _assayHelper.importAssay(ASSAY_MICROSATELLITES, ASSAY_MICROSATELLITES_TSV, getGeneticsPath()); + _assayHelper.importAssay(ASSAY_PHENOTYPES, ASSAY_PHENOTYPES_TSV, getGeneticsPath()); + _assayHelper.importAssay(ASSAY_SNPS, ASSAY_SNPS_TSV, getGeneticsPath()); + } + + private void addExtensibleCols() + { + log("Setup the EHR table definitions"); + EHRAdminPage.beginAt(this, getContainerPath()); + clickAndWait(Locator.linkWithText("Add EHR extensible columns")); + + log("Load EHR table definitions"); + click(Locator.linkWithText("Generate EHR Extensible Columns")); + waitForElement(Locator.tagWithClass("span", "x4-window-header-text").withText("Success")); + assertExt4MsgBox("successfully created columns.", "OK"); + } + + private void completeSnprcPostStudyImportSetup() + { + goToProjectHome(); clickFolder(GENETICSFOLDER); _listHelper.importListArchive(LOOKUP_LIST_ARCHIVE); + clickProject(PROJECT_NAME); PortalHelper portalHelper = new PortalHelper(this); portalHelper.addWebPart("EHR Datasets"); + clickFolder(GENETICSFOLDER); portalHelper.addWebPart("Assay List"); - _assayHelper = new APIAssayHelper(this); - _assayHelper.importAssay(ASSAY_GENE_EXPRESSION, ASSAY_GENE_EXPRESSION_TSV, getGeneticsPath()); - _assayHelper.importAssay(ASSAY_MICROSATELLITES, ASSAY_MICROSATELLITES_TSV, getGeneticsPath()); - _assayHelper.importAssay(ASSAY_PHENOTYPES, ASSAY_PHENOTYPES_TSV, getGeneticsPath()); - _assayHelper.importAssay(ASSAY_SNPS, ASSAY_SNPS_TSV, getGeneticsPath()); } @Override @@ -230,7 +301,7 @@ protected String getExpectedAnimalIDCasing(String id) @Override protected boolean skipStudyImportQueryValidation() { - return true; + return false; } @Override @@ -287,6 +358,141 @@ protected void initSND() goToProjectHome(); } + protected void createSNDCategories() throws CommandException, IOException + { + InsertRowsCommand command = new InsertRowsCommand("snd", "PkgCategories"); + for (String category : SND_CATEGORIES) + { + command.addRow(new HashMap<>(Maps.of( + "Description", category, + "Active", true + ))); + } + + command.execute(createDefaultConnection(), getProjectName()); + } + + protected void createSNDPackages() throws Exception + { + goToProjectHome(); + + Map categoryIds = getSndCategoryIds(); + saveSndPackage(SND_PKG_ID_START, SND_SUPER_PKG_ID_START, "Alopecia", categoryIds.get("Alopecia"), + "Alopecia score: {alopeciaScore}", + Arrays.asList( + sndAttribute("alopeciaScore", "Alopecia Score", "string", false), + sndAttribute("scorer", "Scorer", "string", false))); + saveSndPackage(SND_PKG_ID_START + 1, SND_SUPER_PKG_ID_START + 10, "BCS", categoryIds.get("BCS"), + "BCS: {bcs}", + List.of(sndAttribute("bcs", "BCS", "int", false))); + saveSndPackage(SND_PKG_ID_START + 2, SND_SUPER_PKG_ID_START + 20, "Cumulative Blood", categoryIds.get("Cumulative Blood"), + "Blood volume: {BLOOD_Volume}", + Arrays.asList( + sndAttribute("reason", "Reason", "string", false), + sndAttribute("BLOOD_Volume", "Blood Volume", "double", false))); + saveSndPackage(SND_PKG_ID_START + 3, SND_SUPER_PKG_ID_START + 30, "Vitals Temperature", categoryIds.get("Vitals Temperature"), + "Temperature: {temp}", + List.of(sndAttribute("temp", "Temperature", "double", false))); + saveSndPackage(SND_PKG_ID_START + 4, SND_SUPER_PKG_ID_START + 40, "TB", categoryIds.get("TB"), + "TB result: {tb_result}", + Arrays.asList( + sndAttribute("tb_site", "TB Site", "string", false), + sndAttribute("tb_result", "TB Result", "string", false))); + saveSndPackage(SND_PKG_ID_START + 5, SND_SUPER_PKG_ID_START + 50, "Vitals", categoryIds.get("Vitals"), + "Vitals: {HR}/{RR}/{temp}", + Arrays.asList( + sndAttribute("HR", "Heart Rate", "double", false), + sndAttribute("RR", "Respiratory Rate", "double", false), + sndAttribute("temp", "Temperature", "double", false))); + saveSndPackage(SND_PKG_ID_START + 6, SND_SUPER_PKG_ID_START + 60, "Weight", categoryIds.get("Weight"), + "Weight: {weight} kg", + List.of(sndAttribute("weight", "Weight", "double", false))); + } + + private Map getSndCategoryIds() throws IOException, CommandException + { + SelectRowsCommand command = new SelectRowsCommand("snd", "PkgCategories"); + command.setColumns(Arrays.asList("Description", "CategoryId")); + SelectRowsResponse response = command.execute(createDefaultConnection(), getProjectName()); + + Map categoryIds = new HashMap<>(); + for (Map row : response.getRows()) + { + Object description = row.get("Description"); + Object categoryId = row.get("CategoryId"); + if (description != null && categoryId != null) + { + categoryIds.put(String.valueOf(description), ((Number) categoryId).intValue()); + } + } + + for (String category : SND_CATEGORIES) + { + assertTrue("Missing SND category ID for " + category, categoryIds.containsKey(category)); + } + + return categoryIds; + } + + private Map sndAttribute(String name, String label, String rangeUri, boolean required) + { + return new HashMap<>(Maps.of( + "name", name, + "label", label, + "rangeURI", rangeUri, + "required", required + )); + } + + private void saveSndPackage(int pkgId, int superPkgIdStart, String description, int categoryId, String narrative, List> attributes) + { + String result = (String) executeAsyncScript(buildSaveSndPackageScript(pkgId, superPkgIdStart, description, categoryId, narrative, attributes)); + assertEquals("JavaScript API failure while saving SND package " + description, "Success!", result); + } + + private String buildSaveSndPackageScript(int pkgId, int superPkgIdStart, String description, int categoryId, String narrative, List> attributes) + { + return "LABKEY.Ajax.request({\n" + + " method: 'POST',\n" + + " url: LABKEY.ActionURL.buildURL('snd', 'savePackage.api'),\n" + + " success: function(){ callback('Success!'); },\n" + + " failure: function(e){ callback(e.exception || e.responseText || 'Failed'); },\n" + + " jsonData: {\n" + + " 'id': " + pkgId + ",\n" + + " 'testIdNumberStart': " + superPkgIdStart + ",\n" + + " 'description': '" + jsEscape(description) + "',\n" + + " 'active': true,\n" + + " 'repeatable': true,\n" + + " 'narrative': '" + jsEscape(narrative) + "',\n" + + " 'categories': [" + categoryId + "],\n" + + " 'subPackages': [],\n" + + " 'extraFields': [],\n" + + " 'attributes': " + attributesToJson(attributes) + "\n" + + " }\n" + + "});"; + } + + private String attributesToJson(List> attributes) + { + List values = new ArrayList<>(); + for (Map attribute : attributes) + { + values.add("{" + + "'name': '" + jsEscape(String.valueOf(attribute.get("name"))) + "', " + + "'label': '" + jsEscape(String.valueOf(attribute.get("label"))) + "', " + + "'rangeURI': '" + jsEscape(String.valueOf(attribute.get("rangeURI"))) + "', " + + "'required': " + attribute.get("required") + + "}"); + } + + return "[" + String.join(", ", values) + "]"; + } + + private String jsEscape(String value) + { + return value.replace("\\", "\\\\").replace("'", "\\'"); + } + protected void initGenetics() { beginAt(WebTestHelper.buildURL("ehr", getProjectName(), "doGeneticCalculations"));