diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fff5cbff1..535315bc0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,11 @@ jobs: if: github.repository == 'BimberLabInternal/BimberLabKeyModules' runs-on: ubuntu-latest steps: + # Note: use slight delay in case there are associated commits across repos + - name: "Sleep for 30 seconds" + run: sleep 30s + shell: bash + - name: "Build DISCVR" uses: bimberlabinternal/DevOps/githubActions/discvr-build@master with: diff --git a/IDR/resources/queries/bimber_data/idrOutcomeSource.sql b/IDR/resources/queries/bimber_data/idrOutcomeSource.sql index 7621857ce..d254000ca 100644 --- a/IDR/resources/queries/bimber_data/idrOutcomeSource.sql +++ b/IDR/resources/queries/bimber_data/idrOutcomeSource.sql @@ -6,6 +6,7 @@ cohortStart as date, CASE WHEN contprog = 'C' THEN 'Controller' WHEN contprog = 'P' THEN 'Progressor' + ELSE contprog END as outcome, 'Hansen/IDR' as dataSource diff --git a/IDR/resources/queries/bimber_data/idrSampleSource.sql b/IDR/resources/queries/bimber_data/idrSampleSource.sql index 0a38d53a6..bed1a30ae 100644 --- a/IDR/resources/queries/bimber_data/idrSampleSource.sql +++ b/IDR/resources/queries/bimber_data/idrSampleSource.sql @@ -5,7 +5,10 @@ ID as sampleid, SampleDate as date, Tissue as sampleType, CellCnt as quantity, - +freezer, +rack, +box, +"position", 'Hansen/IDR' as dataSource FROM bimber_data.ln_loc @@ -18,9 +21,17 @@ SELECT Rh as Id, ID as sampleid, SampleDate as date, -Tissue as sampleType, +CASE + WHEN (Tissue IS NOT NULL AND Tissue != '') AND (SampleType IS NOT NULL AND SampleType != '') THEN (SampleType || ' / ' || Tissue) + WHEN (Tissue IS NOT NULL AND Tissue != '') THEN Tissue + WHEN (SampleType IS NOT NULL AND SampleType != '') THEN SampleType + ELSE NULL +END AS sampleType, null as quantity, - +freezer, +rack, +box, +"position", 'Hansen/IDR' as dataSource FROM bimber_data.ult_loc diff --git a/PMR/resources/etls/prime-blooddraws.xml b/PMR/resources/etls/prime-blooddraws.xml index 13abeaa15..e9a49d6d6 100644 --- a/PMR/resources/etls/prime-blooddraws.xml +++ b/PMR/resources/etls/prime-blooddraws.xml @@ -34,6 +34,8 @@ + + diff --git a/PMR/resources/etls/prime-chemistryResults.xml b/PMR/resources/etls/prime-chemistryResults.xml index 0c0cd1c92..10d318563 100644 --- a/PMR/resources/etls/prime-chemistryResults.xml +++ b/PMR/resources/etls/prime-chemistryResults.xml @@ -8,7 +8,6 @@ Id date - ageAtTime testId result units @@ -18,6 +17,9 @@ modified QCState/Label + + + @@ -37,6 +39,8 @@ + + diff --git a/PMR/resources/etls/prime-clinpathRuns.xml b/PMR/resources/etls/prime-clinpathRuns.xml index f6afe0b27..d74ccc6e6 100644 --- a/PMR/resources/etls/prime-clinpathRuns.xml +++ b/PMR/resources/etls/prime-clinpathRuns.xml @@ -19,6 +19,9 @@ modified QCState/Label + + + @@ -38,6 +41,8 @@ + + diff --git a/PMR/resources/etls/prime-hematologyResults.xml b/PMR/resources/etls/prime-hematologyResults.xml index 9df7326e3..4ba39f448 100644 --- a/PMR/resources/etls/prime-hematologyResults.xml +++ b/PMR/resources/etls/prime-hematologyResults.xml @@ -8,7 +8,6 @@ Id date - ageAtTime testId result units @@ -18,6 +17,9 @@ modified QCState/Label + + + @@ -37,6 +39,8 @@ + + diff --git a/PMR/resources/etls/prime-histology.xml b/PMR/resources/etls/prime-histology.xml index 27a21350b..1986eed71 100644 --- a/PMR/resources/etls/prime-histology.xml +++ b/PMR/resources/etls/prime-histology.xml @@ -17,6 +17,9 @@ modified QCState/Label + + + @@ -36,6 +39,8 @@ + + diff --git a/PMR/resources/etls/prime-microbiology.xml b/PMR/resources/etls/prime-microbiology.xml index 194c4d83e..73122f386 100644 --- a/PMR/resources/etls/prime-microbiology.xml +++ b/PMR/resources/etls/prime-microbiology.xml @@ -18,6 +18,9 @@ modified QCState/Label + + + @@ -37,6 +40,8 @@ + + diff --git a/PMR/resources/etls/prime-pathologyDiagnoses.xml b/PMR/resources/etls/prime-pathologyDiagnoses.xml index 428ef6e3a..efed3fb16 100644 --- a/PMR/resources/etls/prime-pathologyDiagnoses.xml +++ b/PMR/resources/etls/prime-pathologyDiagnoses.xml @@ -8,14 +8,14 @@ Id date - ageAtTime sort_order codes objectid - created - modified QCState/Label + + + @@ -35,6 +35,8 @@ + + diff --git a/PMR/resources/etls/prime-weight.xml b/PMR/resources/etls/prime-weight.xml index 435679e81..fd651cf4d 100644 --- a/PMR/resources/etls/prime-weight.xml +++ b/PMR/resources/etls/prime-weight.xml @@ -37,6 +37,8 @@ + + diff --git a/SivStudies/resources/etls/idr-data.xml b/SivStudies/resources/etls/idr-data.xml index aa9aaf086..909ee84ec 100644 --- a/SivStudies/resources/etls/idr-data.xml +++ b/SivStudies/resources/etls/idr-data.xml @@ -15,7 +15,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/SivStudies/resources/etls/idr-subjects.xml b/SivStudies/resources/etls/idr-subjects.xml new file mode 100644 index 000000000..fd084bead --- /dev/null +++ b/SivStudies/resources/etls/idr-subjects.xml @@ -0,0 +1,15 @@ + + + Hansen/IDR Subjects + Hansen/IDR Subjects + + + + + + + + + + + diff --git a/SivStudies/resources/queries/study/additionalDatatypes.query.xml b/SivStudies/resources/queries/study/additionalDatatypes.query.xml index 1b446dfde..45261263a 100644 --- a/SivStudies/resources/queries/study/additionalDatatypes.query.xml +++ b/SivStudies/resources/queries/study/additionalDatatypes.query.xml @@ -18,6 +18,9 @@ Description + + Comments + diff --git a/SivStudies/resources/queries/study/additionalDatatypes/.qview.xml b/SivStudies/resources/queries/study/additionalDatatypes/.qview.xml index 612c76c8a..46110c9f0 100644 --- a/SivStudies/resources/queries/study/additionalDatatypes/.qview.xml +++ b/SivStudies/resources/queries/study/additionalDatatypes/.qview.xml @@ -5,6 +5,7 @@ + diff --git a/SivStudies/resources/queries/study/assignment.query.xml b/SivStudies/resources/queries/study/assignment.query.xml index ed31de818..00df5943a 100644 --- a/SivStudies/resources/queries/study/assignment.query.xml +++ b/SivStudies/resources/queries/study/assignment.query.xml @@ -18,8 +18,8 @@ Sub-Group - - Cohort ID + + Cohort Alias Data Source @@ -27,6 +27,15 @@ Category + + Cohort ID + + studies + studyCohorts + rowId + labelOrName + + Description true diff --git a/SivStudies/resources/queries/study/assignment/.qview.xml b/SivStudies/resources/queries/study/assignment/.qview.xml index b10c793d3..d060c788c 100644 --- a/SivStudies/resources/queries/study/assignment/.qview.xml +++ b/SivStudies/resources/queries/study/assignment/.qview.xml @@ -5,8 +5,9 @@ - + + diff --git a/SivStudies/resources/queries/study/flow.query.xml b/SivStudies/resources/queries/study/flow.query.xml index 63f49ed2f..63a7946a4 100644 --- a/SivStudies/resources/queries/study/flow.query.xml +++ b/SivStudies/resources/queries/study/flow.query.xml @@ -24,7 +24,7 @@ Units - + Comments diff --git a/SivStudies/resources/queries/study/flow/.qview.xml b/SivStudies/resources/queries/study/flow/.qview.xml index c00809cd5..7c9abe0b8 100644 --- a/SivStudies/resources/queries/study/flow/.qview.xml +++ b/SivStudies/resources/queries/study/flow/.qview.xml @@ -7,6 +7,7 @@ + diff --git a/SivStudies/resources/queries/study/genetics.query.xml b/SivStudies/resources/queries/study/genetics.query.xml index 937945710..334a603e5 100644 --- a/SivStudies/resources/queries/study/genetics.query.xml +++ b/SivStudies/resources/queries/study/genetics.query.xml @@ -23,6 +23,10 @@ Score + + Comments + textarea + Data Source diff --git a/SivStudies/resources/queries/study/genetics/.qview.xml b/SivStudies/resources/queries/study/genetics/.qview.xml index bb737b813..0a9321497 100644 --- a/SivStudies/resources/queries/study/genetics/.qview.xml +++ b/SivStudies/resources/queries/study/genetics/.qview.xml @@ -7,6 +7,7 @@ + diff --git a/SivStudies/resources/queries/study/immunizations.query.xml b/SivStudies/resources/queries/study/immunizations.query.xml index 8e2c1528d..7e1a7a603 100644 --- a/SivStudies/resources/queries/study/immunizations.query.xml +++ b/SivStudies/resources/queries/study/immunizations.query.xml @@ -27,6 +27,10 @@ Reason + + Comments + textarea + Data Source diff --git a/SivStudies/resources/queries/study/immunizations/.qview.xml b/SivStudies/resources/queries/study/immunizations/.qview.xml index 8264aae36..575150b0e 100644 --- a/SivStudies/resources/queries/study/immunizations/.qview.xml +++ b/SivStudies/resources/queries/study/immunizations/.qview.xml @@ -7,6 +7,7 @@ + diff --git a/SivStudies/resources/queries/study/labwork.query.xml b/SivStudies/resources/queries/study/labwork.query.xml index c0808ad17..1cb7b4022 100644 --- a/SivStudies/resources/queries/study/labwork.query.xml +++ b/SivStudies/resources/queries/study/labwork.query.xml @@ -34,6 +34,10 @@ Method + + Comments + textarea + Data Source diff --git a/SivStudies/resources/queries/study/labwork/.qview.xml b/SivStudies/resources/queries/study/labwork/.qview.xml index 553672473..800012954 100644 --- a/SivStudies/resources/queries/study/labwork/.qview.xml +++ b/SivStudies/resources/queries/study/labwork/.qview.xml @@ -7,6 +7,7 @@ + diff --git a/SivStudies/resources/queries/study/procedures.query.xml b/SivStudies/resources/queries/study/procedures.query.xml index eaa6a9606..1f5446fe1 100644 --- a/SivStudies/resources/queries/study/procedures.query.xml +++ b/SivStudies/resources/queries/study/procedures.query.xml @@ -15,6 +15,10 @@ Procedure + + Comments + textarea + Data Source diff --git a/SivStudies/resources/queries/study/procedures/.qview.xml b/SivStudies/resources/queries/study/procedures/.qview.xml index 01c43bfd2..a7fb29c4a 100644 --- a/SivStudies/resources/queries/study/procedures/.qview.xml +++ b/SivStudies/resources/queries/study/procedures/.qview.xml @@ -4,6 +4,7 @@ + diff --git a/SivStudies/resources/queries/study/pvlWithoutInfectionDate.query.xml b/SivStudies/resources/queries/study/pvlWithoutInfectionDate.query.xml new file mode 100644 index 000000000..bd636239a --- /dev/null +++ b/SivStudies/resources/queries/study/pvlWithoutInfectionDate.query.xml @@ -0,0 +1,9 @@ + + + + + Animals With PVL Data But No Infection Date +
+
+
+
diff --git a/SivStudies/resources/queries/study/pvlWithoutInfectionDate.sql b/SivStudies/resources/queries/study/pvlWithoutInfectionDate.sql new file mode 100644 index 000000000..1a655cbd8 --- /dev/null +++ b/SivStudies/resources/queries/study/pvlWithoutInfectionDate.sql @@ -0,0 +1,10 @@ +SELECT + vl.Id, + max(vl.result) as maxViralLoad + +FROM study.viralloads vl +WHERE + vl.result IS NOT NULL AND + ((vl.lod is not NULL AND vl.result > vl.lod) OR (vl.lod IS NULL AND vl.result > 50)) AND + vl.timePostSivChallenge.infectionDate IS NULL +GROUP BY vl.Id \ No newline at end of file diff --git a/SivStudies/resources/queries/study/pvl_outcomes.query.xml b/SivStudies/resources/queries/study/pvl_outcomes.query.xml index e06038802..53f67bfff 100644 --- a/SivStudies/resources/queries/study/pvl_outcomes.query.xml +++ b/SivStudies/resources/queries/study/pvl_outcomes.query.xml @@ -5,7 +5,11 @@ - Date + Window Start + Date + + + Window End Date @@ -17,6 +21,10 @@ String Value + + # Datapoints + The number of PVL datapoints used for this calculation + Comments diff --git a/SivStudies/resources/queries/study/pvl_outcomes/.qview.xml b/SivStudies/resources/queries/study/pvl_outcomes/.qview.xml index 05ad5dcfa..8be5fbb72 100644 --- a/SivStudies/resources/queries/study/pvl_outcomes/.qview.xml +++ b/SivStudies/resources/queries/study/pvl_outcomes/.qview.xml @@ -2,6 +2,7 @@ + diff --git a/SivStudies/resources/queries/study/samples.query.xml b/SivStudies/resources/queries/study/samples.query.xml index a65518efc..3d5fc433c 100644 --- a/SivStudies/resources/queries/study/samples.query.xml +++ b/SivStudies/resources/queries/study/samples.query.xml @@ -23,6 +23,22 @@ Quantity Units + + Freezer + + + Rack + + + Box + + + Position + + + Comments + textarea + Data Source diff --git a/SivStudies/resources/queries/study/samples/.qview.xml b/SivStudies/resources/queries/study/samples/.qview.xml index 830bf2440..31300da7c 100644 --- a/SivStudies/resources/queries/study/samples/.qview.xml +++ b/SivStudies/resources/queries/study/samples/.qview.xml @@ -2,10 +2,16 @@ + + + + + + diff --git a/SivStudies/resources/queries/study/studyData.query.xml b/SivStudies/resources/queries/study/studyData.query.xml index e78b0c84f..ab97c463c 100644 --- a/SivStudies/resources/queries/study/studyData.query.xml +++ b/SivStudies/resources/queries/study/studyData.query.xml @@ -7,6 +7,7 @@ http://cpas.labkey.com/Study#ParticipantId laboratory/dataBrowser.view?subjectId=${Id} + ALWAYS_OFF Date diff --git a/SivStudies/resources/queries/study/treatments.query.xml b/SivStudies/resources/queries/study/treatments.query.xml index 52a3ba764..b053767c0 100644 --- a/SivStudies/resources/queries/study/treatments.query.xml +++ b/SivStudies/resources/queries/study/treatments.query.xml @@ -65,6 +65,10 @@ true false + + Comments + textarea + Data Source diff --git a/SivStudies/resources/queries/study/treatments/.qview.xml b/SivStudies/resources/queries/study/treatments/.qview.xml index d9b12386c..070ec43c1 100644 --- a/SivStudies/resources/queries/study/treatments/.qview.xml +++ b/SivStudies/resources/queries/study/treatments/.qview.xml @@ -8,6 +8,7 @@ + diff --git a/SivStudies/resources/queries/study/viralLoads/.qview.xml b/SivStudies/resources/queries/study/viralLoads/.qview.xml index ef089d8f7..3be12098f 100644 --- a/SivStudies/resources/queries/study/viralLoads/.qview.xml +++ b/SivStudies/resources/queries/study/viralLoads/.qview.xml @@ -9,6 +9,7 @@ + diff --git a/SivStudies/resources/queries/study/viralloads.query.xml b/SivStudies/resources/queries/study/viralloads.query.xml index 4e9e9af40..a53069a24 100644 --- a/SivStudies/resources/queries/study/viralloads.query.xml +++ b/SivStudies/resources/queries/study/viralloads.query.xml @@ -39,6 +39,10 @@ true false + + Comments + textarea + Data Source diff --git a/SivStudies/resources/referenceStudy/study/datasets/datasets_metadata.xml b/SivStudies/resources/referenceStudy/study/datasets/datasets_metadata.xml index 5eb90a75a..802e195fc 100644 --- a/SivStudies/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/SivStudies/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -87,6 +87,9 @@ varchar + + varchar + Medications/Treatments @@ -126,6 +129,9 @@ varchar + + varchar + Immunizations @@ -199,6 +205,9 @@ varchar + + varchar + Viral Loads @@ -240,6 +249,9 @@ varchar + + varchar + Lab Results @@ -318,9 +330,12 @@ varchar - + varchar + + integer + varchar @@ -360,6 +375,21 @@ varchar + + varchar + + + varchar + + + varchar + + + varchar + + + varchar + Samples @@ -395,6 +425,9 @@ double + + varchar + Genetic Data @@ -421,6 +454,9 @@ varchar + + varchar + Procedures @@ -447,6 +483,9 @@ varchar + + varchar + Additional Datatypes @@ -482,7 +521,7 @@ varchar - + varchar @@ -527,6 +566,9 @@ timestamp http://cpas.labkey.com/laboratory#sampleDate + + timestamp + varchar @@ -543,6 +585,9 @@ varchar + + integer + varchar diff --git a/SivStudies/resources/views/dataNotes.html b/SivStudies/resources/views/dataNotes.html new file mode 100644 index 000000000..0f56d3005 --- /dev/null +++ b/SivStudies/resources/views/dataNotes.html @@ -0,0 +1,6 @@ +This page summarizes +
    +
  • The fields to calculate timePostSivChallenge require a record in studies.subjectAnchorDates from the same Id where eventLabel = 'SIV Infection'. There may be records in the treatments table for SIV challenges; these do not count
  • +
  • The fields to calculate overlapping PVLs require a record in the viral_load table from the same Id/date, where target = SIV and where sampleType = plasma.
  • +
  • The fields to calculate ART-related date require record(s) in the treatments table from the same Id, where category = 'ART'
  • +
\ No newline at end of file diff --git a/SivStudies/resources/views/studiesAdmin.html b/SivStudies/resources/views/studiesAdmin.html index fb5e91025..8cebaf45b 100644 --- a/SivStudies/resources/views/studiesAdmin.html +++ b/SivStudies/resources/views/studiesAdmin.html @@ -27,6 +27,9 @@ },{ name: 'Notification Admin', url: LABKEY.ActionURL.buildURL('ldk', 'notificationAdmin.view') + },{ + name: 'Notes For Managing Data', + url: LABKEY.ActionURL.buildURL('sivstudies', 'dataNotes.view') }] }] }] diff --git a/SivStudies/src/org/labkey/sivstudies/etl/AddMissingIdrSubjects.java b/SivStudies/src/org/labkey/sivstudies/etl/AddMissingIdrSubjects.java new file mode 100644 index 000000000..fb8f9de4c --- /dev/null +++ b/SivStudies/src/org/labkey/sivstudies/etl/AddMissingIdrSubjects.java @@ -0,0 +1,100 @@ +package org.labkey.sivstudies.etl; + +import org.apache.xmlbeans.XmlException; +import org.jetbrains.annotations.NotNull; +import org.labkey.api.collections.CaseInsensitiveHashSet; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.TableSelector; +import org.labkey.api.di.TaskRefTask; +import org.labkey.api.pipeline.PipelineJob; +import org.labkey.api.pipeline.PipelineJobException; +import org.labkey.api.pipeline.RecordedActionSet; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DuplicateKeyException; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.UserSchema; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.writer.ContainerUser; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class AddMissingIdrSubjects implements TaskRefTask +{ + protected ContainerUser _containerUser; + + @Override + public RecordedActionSet run(@NotNull PipelineJob pipelineJob) throws PipelineJobException + { + // Find existing IDs: + UserSchema us = QueryService.get().getUserSchema(_containerUser.getUser(), _containerUser.getContainer(), "study"); + if (us == null) + { + throw new PipelineJobException("Missing study schema"); + } + + List existingIds = new ArrayList<>(new TableSelector(us.getTable("demographics"), PageFlowUtil.set("Id"), null, null).getArrayList(String.class)); + + // Source IDs: + Container sourceContainer = ContainerManager.getForPath("Labs/Bimber"); + UserSchema us2 = QueryService.get().getUserSchema(_containerUser.getUser(), sourceContainer, "bimber_data"); + if (us2 == null) + { + throw new PipelineJobException("Missing bimber_data schema"); + } + + List allIds = new ArrayList<>(new TableSelector(us2.getTable("subjects"), PageFlowUtil.set("Rh"), null, null).getArrayList(String.class)); + + allIds.removeAll(existingIds); + if (allIds.isEmpty()) + { + return new RecordedActionSet(); + } + + allIds = new ArrayList<>(new CaseInsensitiveHashSet(allIds)); + + pipelineJob.getLogger().info("Creating {} subjects", allIds.size()); + List> toInsert = new ArrayList<>(); + allIds.forEach(id -> { + toInsert.add(Map.of("Id", id)); + }); + + try + { + BatchValidationException bve = new BatchValidationException(); + us.getTable("demographics").getUpdateService().insertRows(_containerUser.getUser(), _containerUser.getContainer(), toInsert, bve, null, null); + if (bve.hasErrors()) + { + throw bve; + } + } + catch (BatchValidationException | SQLException | DuplicateKeyException | QueryUpdateServiceException e) + { + throw new PipelineJobException(e); + } + + return new RecordedActionSet(); + } + + @Override + public List getRequiredSettings() + { + return List.of(); + } + + @Override + public void setSettings(Map map) throws XmlException + { + + } + + @Override + public void setContainerUser(ContainerUser containerUser) + { + _containerUser = containerUser; + } +} diff --git a/SivStudies/src/org/labkey/sivstudies/etl/SubjectScopedSelect.java b/SivStudies/src/org/labkey/sivstudies/etl/SubjectScopedSelect.java index 6c750b8bf..37b54fa8d 100644 --- a/SivStudies/src/org/labkey/sivstudies/etl/SubjectScopedSelect.java +++ b/SivStudies/src/org/labkey/sivstudies/etl/SubjectScopedSelect.java @@ -101,7 +101,7 @@ public boolean isRequired() } } - final int BATCH_SIZE = 250; + final int BATCH_SIZE = 500; private MODE getMode() { diff --git a/SivStudies/src/org/labkey/sivstudies/notification/SivStudiesDataValidationNotification.java b/SivStudies/src/org/labkey/sivstudies/notification/SivStudiesDataValidationNotification.java index 314c662a0..24c03dbbf 100644 --- a/SivStudies/src/org/labkey/sivstudies/notification/SivStudiesDataValidationNotification.java +++ b/SivStudies/src/org/labkey/sivstudies/notification/SivStudiesDataValidationNotification.java @@ -1,20 +1,30 @@ package org.labkey.sivstudies.notification; +import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; +import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; import org.labkey.api.ldk.notification.AbstractNotification; import org.labkey.api.module.ModuleLoader; +import org.labkey.api.query.FieldKey; import org.labkey.api.query.QueryService; import org.labkey.api.query.UserSchema; import org.labkey.api.security.User; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; +import org.labkey.api.util.logging.LogHelper; import org.labkey.sivstudies.SivStudiesModule; +import org.labkey.sivstudies.query.DefaultDatasetTrigger; import java.util.Date; public class SivStudiesDataValidationNotification extends AbstractNotification { + protected static final Logger _log = LogHelper.getLogger(DefaultDatasetTrigger.class, "Messages related to SivStudiesDataValidationNotification"); + public SivStudiesDataValidationNotification() { super(ModuleLoader.getInstance().getModule(SivStudiesModule.class)); @@ -63,6 +73,9 @@ public String getEmailSubject(Container c) Date now = new Date(); duplicateInfectionCheck(c, u, msg); + infectionAnchorDateDiscordance(c, u, msg); + pvlWithoutInfectionDate(c, u, msg); + idsMissingFromDemographics(c, u, msg); if (!msg.isEmpty()) { @@ -91,35 +104,52 @@ private TableInfo getTableInfo(User u, Container c, String schemaName, String qu private void duplicateInfectionCheck(Container c, User u, StringBuilder msg) { - String schemaName = "study"; - String queryName = "duplicateInfectionDates"; + genericQueryCheck(c, u, msg, "study", "duplicateInfectionDates", "duplicate infection date records"); + } + + private void infectionAnchorDateDiscordance(Container c, User u, StringBuilder msg) + { + genericQueryCheck(c, u, msg, "study", "infectionAnchorDateDiscordance", "records with discordant treatment and anchor date SIV infection records"); + } + + private void genericQueryCheck(Container c, User u, StringBuilder msg, String schemaName, String queryName, String message) + { + genericQueryCheck(c, u, msg, schemaName, queryName, message, null); + } + private void genericQueryCheck(Container c, User u, StringBuilder msg, String schemaName, String queryName, String message, @Nullable SimpleFilter filter) + { TableInfo ti = getTableInfo(u, c, schemaName, queryName); - TableSelector ts = new TableSelector(ti); + TableSelector ts = new TableSelector(ti, filter, null); long count = ts.getRowCount(); if (count > 0) { - msg.append("WARNING: There are ").append(count).append(" duplicate infection date records
\n"); + msg.append("WARNING: There are ").append(count).append(" " + message + "
\n"); msg.append("

Click here to view them
\n\n"); msg.append("


\n\n"); } } - private void infectionAnchorDateDiscordance(Container c, User u, StringBuilder msg) + private void pvlWithoutInfectionDate(Container c, User u, StringBuilder msg) { - String schemaName = "study"; - String queryName = "infectionAnchorDateDiscordance"; - - TableInfo ti = getTableInfo(u, c, schemaName, queryName); + genericQueryCheck(c, u, msg, "study", "pvlWithoutInfectionDate", "animals with PVL data but no record of SIV infection"); + } - TableSelector ts = new TableSelector(ti); - long count = ts.getRowCount(); - if (count > 0) + private void idsMissingFromDemographics(Container c, User u, StringBuilder msg) + { + Study s = StudyService.get().getStudy(getTargetContainer(c)); + if (s == null) { - msg.append("WARNING: There are ").append(count).append(" records with discordant treatment and anchor date SIV infection records
\n"); - msg.append("

Click here to view them
\n\n"); - msg.append("


\n\n"); + return; } + + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("DataSet/Demographics/" + s.getSubjectColumnName()), null, CompareType.ISBLANK); + genericQueryCheck(c, u, msg, "study", s.getSubjectNounSingular(), "IDs with data in the study not present in the demographics table", filter); + } + + protected Container getTargetContainer(Container c) + { + return c.isWorkbookOrTab() ? c.getParent() : c; } } diff --git a/SivStudies/src/org/labkey/sivstudies/query/AutoCreateDemographicsTrigger.java b/SivStudies/src/org/labkey/sivstudies/query/AutoCreateDemographicsTrigger.java new file mode 100644 index 000000000..00e5b7394 --- /dev/null +++ b/SivStudies/src/org/labkey/sivstudies/query/AutoCreateDemographicsTrigger.java @@ -0,0 +1,127 @@ +package org.labkey.sivstudies.query; + +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.collections.CaseInsensitiveHashSet; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.TableSelector; +import org.labkey.api.data.triggers.Trigger; +import org.labkey.api.data.triggers.TriggerFactory; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DuplicateKeyException; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; +import org.labkey.api.study.Study; +import org.labkey.api.study.StudyService; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.logging.LogHelper; + +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class AutoCreateDemographicsTrigger extends DefaultDatasetTrigger +{ + protected static final Logger _log = LogHelper.getLogger(DefaultDatasetTrigger.class, "Messages related to CreateDemographicsTrigger"); + + public static class Factory implements TriggerFactory + { + public Factory() + { + + } + + @Override + public @NotNull Collection createTrigger(@Nullable Container c, TableInfo table, Map extraContext) + { + return List.of(new AutoCreateDemographicsTrigger()); + } + } + + private static final String CACHE_KEY = "~~AutoCreateDemographicsTrigger.IdsToCreate~~"; + + @Override + protected void afterUpsert(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + if (extraContext == null) + { + _log.error("extraContext is null in AutoCreateDemographicsTrigger.afterUpsert()"); + return; + } + + if (!extraContext.containsKey(AutoCreateDemographicsTrigger.CACHE_KEY)) + { + extraContext.put(CACHE_KEY, new CaseInsensitiveHashSet()); + } + + if (extraContext.get(CACHE_KEY) instanceof CaseInsensitiveHashSet s) + { + String idField = getIdField(c); + String id = newRow.get(idField) != null ? newRow.get(idField).toString() : null; + if (id != null) + { + s.add(id); + } + } + } + + @Override + public void complete(TableInfo table, Container c, User user, TableInfo.TriggerType event, BatchValidationException errors, Map extraContext) + { + if (extraContext == null) + { + _log.error("extraContext is null in AutoCreateDemographicsTrigger.complete()"); + return; + } + + if (extraContext.get(CACHE_KEY) instanceof CaseInsensitiveHashSet s) + { + s = new CaseInsensitiveHashSet(s); + + String idField = getIdField(c); + TableInfo ti = QueryService.get().getUserSchema(user, getTargetContainer(c), "study").getTable("demographics"); + List existingIds = new TableSelector(ti, PageFlowUtil.set(idField), new SimpleFilter(FieldKey.fromString(idField), s, CompareType.IN), null).getArrayList(String.class); + + s.removeAll(existingIds); + + if (!s.isEmpty()) + { + List> toInsert = s.stream().map(id -> Map.of(idField, (Object)id)).toList(); + try + { + ti.getUpdateService().insertRows(user, c, toInsert, null, null, null); + } + catch (SQLException | BatchValidationException | QueryUpdateServiceException | DuplicateKeyException e) + { + _log.error("Error creating demographics records", e); + } + } + } + } + + private String _idField = null; + + private String getIdField(Container c) + { + if (_idField == null) + { + Study s = StudyService.get().getStudy(getTargetContainer(c)); + if (s == null) + { + return null; + } + + _idField = s.getSubjectColumnName(); + } + + return _idField; + } +} diff --git a/SivStudies/src/org/labkey/sivstudies/query/DefaultDatasetTrigger.java b/SivStudies/src/org/labkey/sivstudies/query/DefaultDatasetTrigger.java new file mode 100644 index 000000000..fea3c8899 --- /dev/null +++ b/SivStudies/src/org/labkey/sivstudies/query/DefaultDatasetTrigger.java @@ -0,0 +1,116 @@ +package org.labkey.sivstudies.query; + +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.triggers.Trigger; +import org.labkey.api.data.triggers.TriggerFactory; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; +import org.labkey.api.util.logging.LogHelper; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class DefaultDatasetTrigger implements Trigger +{ + protected static final Logger _log = LogHelper.getLogger(DefaultDatasetTrigger.class, "Messages related to DefaultDatasetTrigger"); + + public static class Factory implements TriggerFactory + { + public Factory() + { + + } + + @Override + public @NotNull Collection createTrigger(@Nullable Container c, TableInfo table, Map extraContext) + { + return List.of(new DefaultDatasetTrigger()); + } + + } + + @Override + public void beforeInsert(TableInfo table, Container c, User user, @Nullable Map newRow, ValidationException errors, Map extraContext) throws ValidationException + { + beforeInsert(table, c, user, newRow, errors, extraContext, null); + } + + @Override + public void afterInsert(TableInfo table, Container c, User user, @Nullable Map newRow, ValidationException errors, Map extraContext, @Nullable Map existingRecord) throws ValidationException + { + afterUpsert(table, c, user, newRow, existingRecord, errors, extraContext); + } + + @Override + public void afterInsert(TableInfo table, Container c, User user, @Nullable Map newRow, ValidationException errors, Map extraContext) throws ValidationException + { + afterInsert(table, c, user, newRow, errors, extraContext, null); + } + + @Override + public void afterUpdate(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + afterUpsert(table, c, user, newRow, oldRow, errors, extraContext); + } + + protected void afterUpsert(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + + } + + @Override + public void beforeInsert(TableInfo table, Container c, User user, @Nullable Map newRow, ValidationException errors, Map extraContext, @Nullable Map existingRecord) throws ValidationException + { + beforeUpsert(table, c, user, newRow, existingRecord, errors, extraContext); + } + + @Override + public void beforeUpdate(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + beforeUpsert(table, c, user, newRow, oldRow, errors, extraContext); + } + + private void beforeUpsert(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + if (newRow == null) + { + _log.error("newRow was null. Unsure when this would ever happen", new Exception()); + return; + } + + // Simplify properties: + mergeOldToNewRow(newRow, oldRow); + + doBeforeUpsert(table, c, user, newRow, oldRow, errors, extraContext); + } + + protected void doBeforeUpsert(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + // Allow subclasses to implement code here + } + + private void mergeOldToNewRow(@NotNull Map newRow, @Nullable Map oldRow) throws ValidationException + { + if (oldRow != null) + { + for (String propName : oldRow.keySet()) + { + if (!newRow.containsKey(propName) & oldRow.get(propName) != null) + { + newRow.put(propName, oldRow.get(propName)); + } + } + } + } + + protected Container getTargetContainer(Container c) + { + return c.isWorkbookOrTab() ? c.getParent() : c; + } + +} diff --git a/SivStudies/src/org/labkey/sivstudies/query/NumericValuesTrigger.java b/SivStudies/src/org/labkey/sivstudies/query/NumericValuesTrigger.java new file mode 100644 index 000000000..38eac57b8 --- /dev/null +++ b/SivStudies/src/org/labkey/sivstudies/query/NumericValuesTrigger.java @@ -0,0 +1,119 @@ +package org.labkey.sivstudies.query; + +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.triggers.Trigger; +import org.labkey.api.data.triggers.TriggerFactory; +import org.labkey.api.query.SimpleValidationError; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class NumericValuesTrigger extends DefaultDatasetTrigger +{ + public static class Factory implements TriggerFactory + { + // This map allows caller to supply a list of -> . If that string is found in a numeric field, it will be + private final List _stringTransformers; + + public Factory() + { + this(null); + } + + public Factory(@Nullable List stringTransformers) + { + _stringTransformers = stringTransformers == null ? Collections.emptyList() : stringTransformers; + } + + @Override + public @NotNull Collection createTrigger(@Nullable Container c, TableInfo table, Map extraContext) + { + return List.of(new NumericValuesTrigger(_stringTransformers)); + } + } + + private final List _stringTransformers; + + public NumericValuesTrigger(List stringTransformers) + { + _stringTransformers = stringTransformers; + } + + @Override + protected void doBeforeUpsert(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + inspectNumericValues(table, newRow, errors); + } + + private void inspectNumericValues(TableInfo table, Map row, ValidationException errors) + { + for (String propName : row.keySet()) + { + if (row.get(propName) == null) + { + continue; + } + + ColumnInfo ci = table.getColumn(propName); + if (ci == null) + { + continue; + } + + if (!Number.class.isAssignableFrom(ci.getJdbcType().getJavaClass())) + { + continue; + } + + String val = row.get(propName) == null ? null : String.valueOf(row.get(propName)); + if (NumberUtils.isCreatable(val)) + { + return; + } + + // commas are a common problem: + val = val.replaceAll(",", ""); + if (NumberUtils.isCreatable(val)) + { + row.put(propName, val); + return; + } + + // The Rlabkey API sending NAs as strings is another common problem: + if ("NA".equalsIgnoreCase(val) || "null".equalsIgnoreCase(val)) + { + row.put(propName, null); + return; + } + + for (StringTransformer stringTransformer : _stringTransformers) + { + stringTransformer.inspectValue(table, row, val, propName, errors); + val = row.get(propName) == null ? null : String.valueOf(row.get(propName)); + } + + if (val == null || NumberUtils.isCreatable(val)) + { + row.put(propName, val); + return; + } + + errors.addError(new SimpleValidationError("Non-numeric value for field " + propName + ": " + val, propName, ValidationException.SEVERITY.ERROR)); + } + } + + public interface StringTransformer + { + // This method allows code to inspect and modify non-numeric values + public void inspectValue(TableInfo ti, Map row, String stringValue, String propName, ValidationException errors); + } +} diff --git a/SivStudies/src/org/labkey/sivstudies/query/SivStudiesCustomizer.java b/SivStudies/src/org/labkey/sivstudies/query/SivStudiesCustomizer.java index 9d82243ac..1fc15ddbb 100644 --- a/SivStudies/src/org/labkey/sivstudies/query/SivStudiesCustomizer.java +++ b/SivStudies/src/org/labkey/sivstudies/query/SivStudiesCustomizer.java @@ -1,5 +1,6 @@ package org.labkey.sivstudies.query; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.labkey.api.collections.CaseInsensitiveHashSet; import org.labkey.api.data.AbstractTableInfo; @@ -69,10 +70,17 @@ public void performDatasetCustomization(DatasetTable ds) appendDemographicsColumns(ati); + addNumericValuesTrigger(ati); if ("viralLoads".equalsIgnoreCase(ds.getName())) { customizeViralLoads(ati); } + + if ("assignment".equalsIgnoreCase(ds.getName())) + { + ati.addTriggerFactory(StudiesService.get().getStudiesTriggerFactory()); + ati.addTriggerFactory(new AutoCreateDemographicsTrigger.Factory()); + } } else { @@ -425,13 +433,13 @@ public TableInfo getLookupTableInfo() UserSchema targetSchema = ds.getUserSchema().getDefaultSchema().getUserSchema(targetSchemaName); QueryDefinition qd = QueryService.get().createQueryDef(u, targetSchemaContainer, targetSchema, name); qd.setSql("SELECT\n" + - "max(tr.date) as artInitiation,\n" + - "CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY', CAST(max(tr.date) AS DATE), CAST(c." + dateColName + " AS DATE)), INTEGER) as daysPostArtInitiation,\n" + - "CONVERT(age_in_months(CAST(max(tr.date) AS DATE), CAST(c." + dateColName + " AS DATE)), FLOAT) as monthsPostArtInitiation,\n" + + "min(tr.date) as artInitiation,\n" + + "CONVERT(TIMESTAMPDIFF('SQL_TSI_DAY', CAST(min(tr.date) AS DATE), CAST(c." + dateColName + " AS DATE)), INTEGER) as daysPostArtInitiation,\n" + + "CONVERT(age_in_months(CAST(min(tr.date) AS DATE), CAST(c." + dateColName + " AS DATE)), FLOAT) as monthsPostArtInitiation,\n" + "max(tr.enddate) as artRelease,\n" + "CONVERT(CASE WHEN max(tr.enddate) IS NULL THEN NULL ELSE TIMESTAMPDIFF('SQL_TSI_DAY', CAST(max(tr.enddate) AS DATE), CAST(c." + dateColName + " AS DATE)) END, INTEGER) as daysPostArtRelease,\n" + "CONVERT(CASE WHEN max(tr.enddate) IS NULL THEN NULL ELSE age_in_months(CAST(max(tr.enddate) AS DATE), CAST(c." + dateColName + " AS DATE)) END, FLOAT) as monthsPostArtRelease,\n" + - "CAST(CASE WHEN CAST(max(tr.date) AS DATE) < CAST(c." + dateCol.getFieldKey().toString() + " AS DATE) AND CAST(max(coalesce(tr.enddate, now())) AS DATE) >= CAST(c." + dateCol.getFieldKey().toString() + " AS DATE) THEN 'Y' ELSE null END as VARCHAR) as onArt,\n" + + "CAST(CASE WHEN CAST(min(tr.date) AS DATE) <= CAST(c." + dateCol.getFieldKey().toString() + " AS DATE) AND CAST(max(coalesce(tr.enddate, now())) AS DATE) >= CAST(c." + dateCol.getFieldKey().toString() + " AS DATE) THEN 'Y' ELSE null END as VARCHAR) as onArt,\n" + "GROUP_CONCAT(DISTINCT tr.treatment) AS artTreatment,\n" + "c." + pkCol.getFieldKey().toString() + "\n" + "FROM \"" + schemaName + "\".\"" + queryName + "\" c " + @@ -494,6 +502,31 @@ private BaseColumnInfo getWrappedIdCol(UserSchema targetQueryUserSchema, String return col; } + private void addNumericValuesTrigger(AbstractTableInfo ati) + { + // This behavior conflicts with ViralLoadsTriggerFactory + if ("viralLoads".equalsIgnoreCase(ati.getName())) + { + return; + } + + List stringTransformers = new ArrayList<>(); + if ("immunizations".equalsIgnoreCase(ati.getName())) + { + stringTransformers.add((ti, row, stringValue, propName, errors) -> { + if ("quantity".equalsIgnoreCase(propName) & ("Supernatant".equalsIgnoreCase(stringValue) | "Supernatent".equalsIgnoreCase(stringValue))) + { + row.put("quantity", null); + String comments = row.get("comments") == null ? null : StringUtils.trimToNull(String.valueOf(row.get("comments"))); + comments = (comments == null ? "" : comments + ", ") + "Quantity: Supernatant"; + row.put("comments", comments); + } + }); + } + + ati.addTriggerFactory(new NumericValuesTrigger.Factory(stringTransformers)); + } + private void customizeViralLoads(AbstractTableInfo ati) { ati.addTriggerFactory(new ViralLoadsTriggerFactory()); diff --git a/mGAP/resources/views/phenotypes.html b/mGAP/resources/views/phenotypes.html index e97e8e0c3..eb1f0e6ad 100644 --- a/mGAP/resources/views/phenotypes.html +++ b/mGAP/resources/views/phenotypes.html @@ -16,7 +16,7 @@ ['Nervous system','Vision','Coats-like retinopathy','','','Liu et al., 2015:25656754'], ['Nervous system','Neurological','Batten disease','CLN7','c.769delA; p.Ile257LeufsTer36','McBride et al., 2018:30048804'], ['Nervous system','Neurological','Krabbe disease','GALC','c.435_436delAC; p.Leu146fs','Luzi et al., 1997:9192853;Baskin et al., 1998:10090061', 'Hordeaux et al., 2022:35333110'], - ['Nervous system','Neurological','Pelizaiaeus-Merzbacher disease','PLP1','c.682 T > C; p.Cys228Arg','Sherman et al., 2021:34364975'], + ['Nervous system','Neurological','Pelizaeus-Merzbacher disease','PLP1','c.682 T > C; p.Cys228Arg','Sherman et al., 2021:34364975'], ['Nervous system','Neurological','Epilepsy','','','Salinas et al., 2015:26290449;Akos Szabo et al., 2019:31592545'], ['Nervous system','Psychiatric','Naltrexone response','OPRM1','c.77C>G; p.Pro26Arg','Vallender et al., 2010:20153935'], ['Nervous system','Psychiatric','Anxiety ','5-HTT','5-HTTLPR','Spinelli et al., 2012:22293001'], diff --git a/mGAP/src/org/labkey/mgap/mGAPModule.java b/mGAP/src/org/labkey/mgap/mGAPModule.java index 8ba8ba931..b702a8308 100644 --- a/mGAP/src/org/labkey/mgap/mGAPModule.java +++ b/mGAP/src/org/labkey/mgap/mGAPModule.java @@ -111,6 +111,7 @@ public void doStartupAfterSpringConfig(ModuleContext moduleContext) ContentSecurityPolicyFilter.registerAllowedSources(this.getClass().getName(), Directive.Connection, "https://code.jquery.com", "https://*.fontawesome.com"); ContentSecurityPolicyFilter.registerAllowedSources(this.getClass().getName(), Directive.Style, "https://code.jquery.com", "https://www.gstatic.com"); ContentSecurityPolicyFilter.registerAllowedSources(this.getClass().getName(), Directive.Font, "https://*.fontawesome.com"); + ContentSecurityPolicyFilter.registerAllowedSources(this.getClass().getName(), Directive.Connection, "https://oss.maxcdn.com"); new PipelineStartup(); } diff --git a/mcc/package-lock.json b/mcc/package-lock.json index b20dd7fb3..a799d2232 100644 --- a/mcc/package-lock.json +++ b/mcc/package-lock.json @@ -8193,10 +8193,11 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", diff --git a/mcc/resources/etls/mcc.xml b/mcc/resources/etls/mcc.xml index 9e3d71ad9..e5a788736 100644 --- a/mcc/resources/etls/mcc.xml +++ b/mcc/resources/etls/mcc.xml @@ -38,7 +38,7 @@ objectid - + @@ -86,6 +86,7 @@ date datatype sra_accession + total_reads objectid diff --git a/mcc/resources/etls/snprc-datasets.xml b/mcc/resources/etls/snprc-datasets.xml index 9b66a4814..350a55d38 100644 --- a/mcc/resources/etls/snprc-datasets.xml +++ b/mcc/resources/etls/snprc-datasets.xml @@ -155,7 +155,7 @@ Copy to target - + AnimalId Date diff --git a/mcc/resources/folderTypes/MCC NIH Dashboard.folderType.xml b/mcc/resources/folderTypes/MCC NIH Dashboard.folderType.xml index 8bf4b4864..ae9d9e26c 100644 --- a/mcc/resources/folderTypes/MCC NIH Dashboard.folderType.xml +++ b/mcc/resources/folderTypes/MCC NIH Dashboard.folderType.xml @@ -74,6 +74,21 @@ + + geneticsDashboard + Genetics + + + genetics + + + + + Marmoset Genetics + body + + + requests MCC Requests diff --git a/mcc/resources/queries/mcc/genomicDatasetsSource.sql b/mcc/resources/queries/mcc/genomicDatasetsSource.sql index 1dcdba771..332e76afc 100644 --- a/mcc/resources/queries/mcc/genomicDatasetsSource.sql +++ b/mcc/resources/queries/mcc/genomicDatasetsSource.sql @@ -3,7 +3,8 @@ SELECT r.subjectid as Id, r.created as date, r.application as datatype, - r.sraRuns as sra_accession + r.sraRuns as sra_accession, + r.totalForwardReads as total_reads FROM sequenceanalysis.sequence_readsets r diff --git a/mcc/resources/queries/study/genomicDatasets.query.xml b/mcc/resources/queries/study/genomicDatasets.query.xml index c78b85463..fd9406c2c 100644 --- a/mcc/resources/queries/study/genomicDatasets.query.xml +++ b/mcc/resources/queries/study/genomicDatasets.query.xml @@ -18,6 +18,10 @@ SRA Accession https://trace.ncbi.nlm.nih.gov/Traces/sra/?run=${sra_accession}
+ + Total Reads + #,##0.## + diff --git a/mcc/resources/queries/study/genomicDatasets/.qview.xml b/mcc/resources/queries/study/genomicDatasets/.qview.xml index e583f4f41..c29532b3c 100644 --- a/mcc/resources/queries/study/genomicDatasets/.qview.xml +++ b/mcc/resources/queries/study/genomicDatasets/.qview.xml @@ -4,6 +4,7 @@ + diff --git a/mcc/resources/referenceStudy/study/datasets/datasets_metadata.xml b/mcc/resources/referenceStudy/study/datasets/datasets_metadata.xml index d1ce17b9d..60b6cda93 100644 --- a/mcc/resources/referenceStudy/study/datasets/datasets_metadata.xml +++ b/mcc/resources/referenceStudy/study/datasets/datasets_metadata.xml @@ -1073,6 +1073,9 @@ varchar + + integer + entityid urn:ehr.labkey.org/#ObjectId diff --git a/mcc/resources/schemas/dbscripts/postgresql/mcc-20.018-20.019.sql b/mcc/resources/schemas/dbscripts/postgresql/mcc-20.018-20.019.sql new file mode 100644 index 000000000..67506d33e --- /dev/null +++ b/mcc/resources/schemas/dbscripts/postgresql/mcc-20.018-20.019.sql @@ -0,0 +1 @@ +ALTER TABLE mcc.animalRequests ADD shippingAcknowledgement bool; diff --git a/mcc/resources/schemas/dbscripts/sqlserver/mcc-20.018-20.019.sql b/mcc/resources/schemas/dbscripts/sqlserver/mcc-20.018-20.019.sql new file mode 100644 index 000000000..aa155ac90 --- /dev/null +++ b/mcc/resources/schemas/dbscripts/sqlserver/mcc-20.018-20.019.sql @@ -0,0 +1 @@ +ALTER TABLE mcc.animalRequests ADD shippingAcknowledgement bit; diff --git a/mcc/resources/schemas/mcc.xml b/mcc/resources/schemas/mcc.xml index 86970ae21..0a0403e4f 100644 --- a/mcc/resources/schemas/mcc.xml +++ b/mcc/resources/schemas/mcc.xml @@ -373,6 +373,10 @@ IACUC Protocol # true + + Shipping Acknowledgement Entered? + true + Other Comments true diff --git a/mcc/resources/views/mccRequests.html b/mcc/resources/views/mccRequests.html index 53e77dce9..f47a1ee71 100644 --- a/mcc/resources/views/mccRequests.html +++ b/mcc/resources/views/mccRequests.html @@ -8,7 +8,7 @@ Ext4.get(webpart.wrapperDivId).update( 'The MCC is now accepting animal requests! Investigators interested in requesting marmosets for their research can submit applications via the animal request portal.

' + - 'Investigators can anticipate the estimated cost for getting marmosets to be about $5,500 USD per animal plus approximately $10,000 USD for shipping. Please note that this is an estimate and the actual cost is determined by the breeding centers and shipping can vary based on distance.' + + 'Investigators can anticipate the following estimated cost per animal: $5,500 USD for early-stage investigators, $6,500 USD for other academic investigators and $10,000 USD for commercial institutions. Arranging shipping is the responsibility of the requestor and is approximately $10,000 USD. Please note that this is an estimate and the actual cost is determined by the breeding centers and shipping can vary based on distance.' + '

' + 'Submit New Animal Request
' + 'Click Here to View Documentation on the Request Process and Scoring Criteria' + diff --git a/mcc/resources/views/mccU24Demographics.html b/mcc/resources/views/mccU24Demographics.html index e490ff8d5..5afcd1cd4 100644 --- a/mcc/resources/views/mccU24Demographics.html +++ b/mcc/resources/views/mccU24Demographics.html @@ -12,7 +12,10 @@ title: 'MCC Animals', schemaName: 'study', queryName: 'demographics', - viewName: LABKEY.ActionURL.getParameter('viewName') ?? 'U24 Assigned' + viewName: LABKEY.ActionURL.getParameter('viewName'), + removeableFilters: [ + LABKEY.Filter.create('u24_status', true, LABKEY.Filter.Types.EQUALS) + ] }).render(webpart.wrapperDivId); }); diff --git a/mcc/resources/web/mcc/window/MarkShippedWindow.js b/mcc/resources/web/mcc/window/MarkShippedWindow.js index afa44eaad..9cbf6818e 100644 --- a/mcc/resources/web/mcc/window/MarkShippedWindow.js +++ b/mcc/resources/web/mcc/window/MarkShippedWindow.js @@ -208,7 +208,6 @@ Ext4.define('MCC.window.MarkShippedWindow', { return; } - var targetFolderId = win.down('#targetFolder').store.findRecord('Path', targetFolder).get('EntityId'); Ext4.Msg.wait('Saving...'); LABKEY.Query.selectRows({ schemaName: 'study', @@ -224,143 +223,229 @@ Ext4.define('MCC.window.MarkShippedWindow', { return false; } - var commands = []; - Ext4.Array.forEach(results.rows, function(row){ - var effectiveId = win.down('#usePreviousId-' + row.Id).getValue() ? row.Id : win.down('#newId-' + row.Id).getValue(); - var requestId = win.down('#requestId-' + row.Id).getValue(); - // This should be checked above, although perhaps case sensitivity could get involved: - LDK.Assert.assertNotEmpty('Missing effective ID after query', effectiveId); - - var shouldAddDeparture = !row['Id/MostRecentDeparture/MostRecentDeparture'] || - row['Id/MostRecentDeparture/MostRecentDeparture'] !== Ext4.Date.format(row.effectiveDate, 'Y-m-d') || - row['Id/MostRecentDeparture/mccRequestId'] !== requestId || - row.Id !== effectiveId; - if (shouldAddDeparture) { - commands.push({ - command: 'insert', - schemaName: 'study', - queryName: 'Departure', - rows: [{ - Id: row.Id, - date: effectiveDate, - source: row.colony, - destination: centerName, - mccRequestId: requestId, - description: row.colony ? 'Original center: ' + row.colony : null, - qcstate: null, - objectId: null, - QCStateLabel: 'Completed' - }] - }); - } + var uniqueIds = []; + Ext4.Array.forEach(results.rows, function(row) { + uniqueIds.push(win.down('#usePreviousId-' + row.Id).getValue() ? row.Id : win.down('#newId-' + row.Id).getValue()); + }, this); - // If going to a new LK folder, we're creating a whole new record: - if (targetFolderId.toUpperCase() !== LABKEY.Security.currentContainer.id.toUpperCase() || effectiveId !== row.Id) { - commands.push({ - command: 'insert', - containerPath: targetFolder, - schemaName: 'study', - queryName: 'Demographics', - rows: [{ - Id: effectiveId, - date: effectiveDate, - alternateIds: row.Id !== effectiveId ? row.Id : null, - gender: row.gender, - species: row.species, - birth: row.birth, - death: row.death, - dam: row.dam, - sire: row.sire, - damMccAlias: row['damMccAlias/externalAlias'], - sireMccAlias: row['sireMccAlias/externalAlias'], - colony: centerName, - source: row.colony, - calculated_status: 'Alive', - mccAlias: row['Id/mccAlias/externalAlias'], - QCState: null, - QCStateLabel: 'Completed', - objectId: null - }] - }); - - commands.push({ - command: 'update', - containerPath: null, //Use current folder - schemaName: 'study', - queryName: 'Demographics', - rows: [{ - Id: row.Id, // NOTE: always change the original record - excludeFromCensus: true - }] - }); - } - else { - // Otherwise update the existing: - commands.push({ - command: 'update', - containerPath: targetFolder, - schemaName: 'study', - queryName: 'Demographics', - rows: [{ - Id: row.Id, - date: effectiveDate, - alternateIds: null, - gender: row.gender, - species: row.species, - birth: row.birth, - death: row.death, - dam: row.dam, - sire: row.sire, - colony: centerName, - source: row.colony, - calculated_status: 'Alive', - QCState: null, - QCStateLabel: 'Completed', - objectId: null - }] - }); + LABKEY.Query.selectRows({ + schemaName: 'study', + queryName: 'Demographics', + containerPath: targetFolder, + filterArray: [LABKEY.Filter.create('Id', uniqueIds.join(';'), LABKEY.Filter.Types.IN)], + columns: 'Id,gender,species,birth,death,dam,sire,damMccAlias/externalAlias,sireMccAlias/externalAlias,calculated_status,Id/mccAlias/externalAlias,colony,source,lsid,objectid', + scope: this, + failure: LDK.Utils.getErrorCallback(), + success: function(existingIdResults) { + var preexistingIdsInTargetFolder = {}; + Ext4.Array.forEach(existingIdResults.rows, function(r){ + preexistingIdsInTargetFolder[r.Id] = r; + }, this); + + this.doSave(win, results, preexistingIdsInTargetFolder); } + }); + } + }); + }, + + doSave: function(win, results, preexistingIdsInTargetFolder){ + var effectiveDate = win.down('#effectiveDate').getValue(); + var centerName = win.down('#centerName').getValue(); + var targetFolder = win.down('#targetFolder').getValue(); + var targetFolderId = win.down('#targetFolder').store.findRecord('Path', targetFolder).get('EntityId'); + + var commands = []; + var hadError = false; + Ext4.Array.forEach(results.rows, function(row){ + var effectiveId = win.down('#usePreviousId-' + row.Id).getValue() ? row.Id : win.down('#newId-' + row.Id).getValue(); + var requestId = win.down('#requestId-' + row.Id).getValue(); + // This should be checked above, although perhaps case sensitivity could get involved: + LDK.Assert.assertNotEmpty('Missing effective ID after query', effectiveId); + + var shouldAddDeparture = !row['Id/MostRecentDeparture/MostRecentDeparture'] || + row['Id/MostRecentDeparture/MostRecentDeparture'] !== Ext4.Date.format(row.effectiveDate, 'Y-m-d') || + row['Id/MostRecentDeparture/mccRequestId'] !== requestId || + row.Id !== effectiveId; + if (shouldAddDeparture) { + commands.push({ + command: 'insert', + schemaName: 'study', + queryName: 'Departure', + rows: [{ + Id: row.Id, + date: effectiveDate, + source: row.colony, + destination: centerName, + mccRequestId: requestId, + description: row.colony ? 'Original center: ' + row.colony : null, + qcstate: null, + objectId: null, + QCStateLabel: 'Completed' + }] + }); + } - var shouldAddArrival = !row['Id/MostRecentArrival/MostRecentArrival'] || - row['Id/MostRecentArrival/MostRecentArrival'] !== Ext4.Date.format(row.effectiveDate, 'Y-m-d') || - row['Id/MostRecentArrival/mccRequestId'] !== requestId || - row.Id !== effectiveId; - if (shouldAddArrival) { - // And also add an arrival record. NOTE: set the date after the departure to get status to update properly - var arrivalDate = new Date(effectiveDate).setMinutes(effectiveDate.getMinutes() + 1); - commands.push({ - command: 'insert', - containerPath: targetFolder, - schemaName: 'study', - queryName: 'Arrival', - rows: [{ - Id: effectiveId, - date: arrivalDate, - source: centerName, - mccRequestId: requestId, - QCState: null, - QCStateLabel: 'Completed', - objectId: null - }] - }); + // If going to a new LK folder, we're creating a whole new record: + if (targetFolderId.toUpperCase() !== LABKEY.Security.currentContainer.id.toUpperCase() || effectiveId !== row.Id) { + if (Ext4.Object.getKeys(preexistingIdsInTargetFolder).indexOf(effectiveId) === -1) { + // No existing record for this ID, make new record: + commands.push({ + command: 'insert', + containerPath: targetFolder, + schemaName: 'study', + queryName: 'Demographics', + rows: [{ + Id: effectiveId, + date: effectiveDate, + alternateIds: row.Id !== effectiveId ? row.Id : null, + gender: row.gender, + species: row.species, + birth: row.birth, + death: row.death, + dam: row.dam, + sire: row.sire, + damMccAlias: row['damMccAlias/externalAlias'], + sireMccAlias: row['sireMccAlias/externalAlias'], + colony: centerName, + source: row.colony, + calculated_status: 'Alive', + mccAlias: row['Id/mccAlias/externalAlias'], + QCState: null, + QCStateLabel: 'Completed', + objectId: null + }] + }); + } + else { + // There is an existing record for this ID, so merge/validate: + console.log('Existing record found for: ' + effectiveId) + var toUpdate = preexistingIdsInTargetFolder[effectiveId] + + var errors = [] + Ext4.Array.forEach(['gender', 'species', 'birth', 'death', 'dam', 'sire'], function(fieldName) { + this.doFieldCheck(row, fieldName, toUpdate, fieldName, errors, effectiveId) + }, this); + + toUpdate.colony = centerName + toUpdate.source = toUpdate.source || row.colony + toUpdate.calculated_status = toUpdate.calculated_status || 'Alive'; + + if (row.Id !== effectiveId) { + toUpdate.alternateIds = toUpdate.alternateIds ? toUpdate.alternateIds + ',' + row.Id : row.Id; } - }, this); - LABKEY.Query.saveRows({ - commands: commands, - scope: this, - failure: LDK.Utils.getErrorCallback(), - success: function() { - Ext4.Msg.hide(); - Ext4.Msg.alert('Success', 'Transfer Added', function () { - var dataRegion = LABKEY.DataRegions[this.dataRegionName]; - this.destroy(); + this.doFieldCheck(row, 'damMccAlias/externalAlias', toUpdate, 'damMccAlias', errors, effectiveId) + this.doFieldCheck(row, 'sireMccAlias/externalAlias', toUpdate, 'sireMccAlias', errors, effectiveId) + this.doFieldCheck(row, 'Id/mccAlias/externalAlias', toUpdate, 'mccAlias', errors, effectiveId) - dataRegion.refresh(); - }, this); + if (errors.length) { + Ext4.Msg.hide(); + Ext4.Msg.alert('Error', 'Inconsistent data between source and destination demographics for: ' + effectiveId + + '
' + errors.join('
')); + hadError = true; + return false; } + + commands.push({ + command: 'update', + containerPath: targetFolder, + schemaName: 'study', + queryName: 'Demographics', + rows: [toUpdate] + }); + } + + commands.push({ + command: 'update', + containerPath: null, //Use current folder + schemaName: 'study', + queryName: 'Demographics', + rows: [{ + Id: row.Id, // NOTE: always change the original record + excludeFromCensus: true + }] }); } + else { + // Otherwise update the existing: + commands.push({ + command: 'update', + containerPath: targetFolder, + schemaName: 'study', + queryName: 'Demographics', + rows: [{ + Id: row.Id, + date: effectiveDate, + alternateIds: null, + gender: row.gender, + species: row.species, + birth: row.birth, + death: row.death, + dam: row.dam, + sire: row.sire, + colony: centerName, + source: row.colony, + calculated_status: 'Alive', + QCState: null, + QCStateLabel: 'Completed', + objectId: null + }] + }); + } + + var shouldAddArrival = !row['Id/MostRecentArrival/MostRecentArrival'] || + row['Id/MostRecentArrival/MostRecentArrival'] !== Ext4.Date.format(row.effectiveDate, 'Y-m-d') || + row['Id/MostRecentArrival/mccRequestId'] !== requestId || + row.Id !== effectiveId; + if (shouldAddArrival) { + // And also add an arrival record. NOTE: set the date after the departure to get status to update properly + var arrivalDate = new Date(effectiveDate).setMinutes(effectiveDate.getMinutes() + 1); + commands.push({ + command: 'insert', + containerPath: targetFolder, + schemaName: 'study', + queryName: 'Arrival', + rows: [{ + Id: effectiveId, + date: arrivalDate, + source: centerName, + mccRequestId: requestId, + QCState: null, + QCStateLabel: 'Completed', + objectId: null + }] + }); + } + }, this); + + if (hadError) { + return; + } + + LABKEY.Query.saveRows({ + commands: commands, + scope: this, + failure: LDK.Utils.getErrorCallback(), + success: function() { + Ext4.Msg.hide(); + Ext4.Msg.alert('Success', 'Transfer Added', function () { + var dataRegion = LABKEY.DataRegions[this.dataRegionName]; + this.destroy(); + + dataRegion.refresh(); + }, this); + } }); + }, + + doFieldCheck: function(row, fieldName1, toUpdate, fieldName2, errors, effectiveId) { + if (row[fieldName1]) { + if (toUpdate[fieldName2] && toUpdate[fieldName2] !== row[fieldName1]) { + errors.push('Pre-existing record for ' + effectiveId + ', but ' + fieldName2 + ' was inconsistent between old/new (' + toUpdate[fieldName2] + '/' + row[fieldName1] + ')') + } + else { + toUpdate[fieldName2] = row[fieldName1]; + } + } } }); \ No newline at end of file diff --git a/mcc/src/client/AnimalRequest/animal-request.tsx b/mcc/src/client/AnimalRequest/animal-request.tsx index e09acd148..fc79fd362 100644 --- a/mcc/src/client/AnimalRequest/animal-request.tsx +++ b/mcc/src/client/AnimalRequest/animal-request.tsx @@ -44,7 +44,8 @@ import { institutionTypeOptions, methodsProposedPlaceholder, signingOfficialTooltip, - terminalProceduresLabel + terminalProceduresLabel, + shippingAcknowledgementStatement } from './components/values'; import AnimalCensus from './components/census'; @@ -330,6 +331,7 @@ export function AnimalRequest() { "grantnumber" : data.get("funding-grant-number"), "applicationduedate": data.get("funding-application-due-date"), "comments": data.get("comments"), + "shippingAcknowledgement": !!data.get("shippingAcknowledgement"), "status": requestData.request.status, }] }, @@ -461,308 +463,395 @@ export function AnimalRequest() { return ( <> -
-

Overview

+ +

Overview

- - <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <Input id="project-title" ariaLabel="Title" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} placeholder="Project Title" defaultValue={requestData.request.title}/> - </ErrorMessageHandler> - </div> - - <Title text="2. Project Narrative*"/> - <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <TextArea id="project-narrative" ariaLabel="Project Narrative" isSubmitting={isSubmitting} placeholder="Project Narrative" required={doEnforceRequiredFields()} defaultValue={requestData.request.narrative}/> - </ErrorMessageHandler> - </div> - - <Title text="3. Research/Disease Focus*"/> - <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <Input id="diseasefocus" ariaLabel="Research/Disease Focus" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} placeholder="What is the research area or disease focus of this project" defaultValue={requestData.request.diseasefocus}/> - </ErrorMessageHandler> - </div> - - <Title text="4. How does the research relate to neuroscience?*"/> - <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <TextArea id="neuroscience" ariaLabel="Connection to neuroscience" isSubmitting={isSubmitting} placeholder="How does the research relate to neuroscience" required={doEnforceRequiredFields()} defaultValue={requestData.request.neuroscience}/> - </ErrorMessageHandler> - </div> - - <h3>General Information</h3> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <Title text="1. Principal Investigator*"/> - - <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="investigator-last-name" ariaLabel="Last Name" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} placeholder="Last Name" defaultValue={requestData.request.lastname}/> - </div> - - <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="investigator-first-name" ariaLabel="First Name" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} placeholder="First Name" defaultValue={requestData.request.firstname}/> - </div> - - <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="investigator-middle-initial" isSubmitting={isSubmitting} required={false} placeholder="Middle Initial" defaultValue={requestData.request.middleinitial} maxLength="8"/> - </div> - </div> - </ErrorMessageHandler> - - <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> - <Title text="2. Are you an early-stage investigator? "/> - <Tooltip id="early-stage-investigator-helper" - text={earlyInvestigatorTooltip} - /> - </div> - - <div className="tw-w-full tw-px-3 tw-mt-6"> - <YesNoRadio id="is-early-stage-investigator" ariaLabel="Early Stage Investigator" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} defaultValue={requestData.request.earlystageinvestigator}/> + <Title text="1. Project Title*"/> + <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <Input id="project-title" ariaLabel="Title" isSubmitting={isSubmitting} + required={doEnforceRequiredFields()} placeholder="Project Title" + defaultValue={requestData.request.title}/> + </ErrorMessageHandler> </div> - </div> - </ErrorMessageHandler> - - <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <Title text="3. Affiliated research institution*"/> + <Title text="2. Project Narrative*"/> <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="institution-name" ariaLabel="Institution Name" isSubmitting={isSubmitting} placeholder="Name" required={doEnforceRequiredFields()} defaultValue={requestData.request.institutionname}/> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <TextArea id="project-narrative" ariaLabel="Project Narrative" isSubmitting={isSubmitting} + placeholder="Project Narrative" required={doEnforceRequiredFields()} + defaultValue={requestData.request.narrative}/> + </ErrorMessageHandler> </div> - <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="institution-city" ariaLabel="Institution City" isSubmitting={isSubmitting} placeholder="City" required={doEnforceRequiredFields()} defaultValue={requestData.request.institutioncity}/> + <Title text="3. Research/Disease Focus*"/> + <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <Input id="diseasefocus" ariaLabel="Research/Disease Focus" isSubmitting={isSubmitting} + required={doEnforceRequiredFields()} + placeholder="What is the research area or disease focus of this project" + defaultValue={requestData.request.diseasefocus}/> + </ErrorMessageHandler> </div> - <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="institution-state" ariaLabel="Institution State" isSubmitting={isSubmitting} placeholder="State" required={doEnforceRequiredFields()} defaultValue={requestData.request.institutionstate}/> + <Title text="4. How does the research relate to neuroscience?*"/> + <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <TextArea id="neuroscience" ariaLabel="Connection to neuroscience" isSubmitting={isSubmitting} + placeholder="How does the research relate to neuroscience" + required={doEnforceRequiredFields()} defaultValue={requestData.request.neuroscience}/> + </ErrorMessageHandler> </div> - <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="institution-country" ariaLabel="Institution Country" isSubmitting={isSubmitting} placeholder="Country" required={doEnforceRequiredFields()} defaultValue={requestData.request.institutioncountry}/> - </div> + <h3>General Information</h3> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> + <Title text="1. Principal Investigator*"/> - <Title text="4. Affiliated Research Institution Type*"/> + <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="investigator-last-name" ariaLabel="Last Name" isSubmitting={isSubmitting} + required={doEnforceRequiredFields()} placeholder="Last Name" + defaultValue={requestData.request.lastname}/> + </div> - <div className="tw-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Select id="institution-type" ariaLabel="Institution Type" isSubmitting={isSubmitting} placeholder="Type" required={doEnforceRequiredFields()} defaultValue={requestData.request.institutiontype} options={institutionTypeOptions}/> - </div> - </div> - </ErrorMessageHandler> + <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="investigator-first-name" ariaLabel="First Name" isSubmitting={isSubmitting} + required={doEnforceRequiredFields()} placeholder="First Name" + defaultValue={requestData.request.firstname}/> + </div> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> - <Title text="5. Institution Signing Official* "/> - <Tooltip id="signing-official-helper" - text={signingOfficialTooltip} - /> - </div> + <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="investigator-middle-initial" isSubmitting={isSubmitting} required={false} + placeholder="Middle Initial" defaultValue={requestData.request.middleinitial} + maxLength="8"/> + </div> + </div> + </ErrorMessageHandler> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> + <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> + <Title text="2. Are you an early-stage investigator? "/> + <Tooltip id="early-stage-investigator-helper" + text={earlyInvestigatorTooltip} + /> + </div> - <div className="tw-flex tw-flex-wrap tw-mt-6"> - <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="official-last-name" ariaLabel="Last Name" isSubmitting={isSubmitting} placeholder="Last Name" required={doEnforceRequiredFields()} defaultValue={requestData.request.officiallastname}/> + <div className="tw-w-full tw-px-3 tw-mt-6"> + <YesNoRadio id="is-early-stage-investigator" ariaLabel="Early Stage Investigator" + isSubmitting={isSubmitting} required={doEnforceRequiredFields()} + defaultValue={requestData.request.earlystageinvestigator}/> + </div> </div> + </ErrorMessageHandler> - <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="official-first-name" ariaLabel="First Name" isSubmitting={isSubmitting} placeholder="First Name" required={doEnforceRequiredFields()} defaultValue={requestData.request.officialfirstname}/> - </div> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> + <Title text="3. Affiliated research institution*"/> - <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="official-email" ariaLabel="Email Address" isSubmitting={isSubmitting} placeholder="Email Address" required={doEnforceRequiredFields()} defaultValue={requestData.request.officialemail}/> - </div> - </div> - </div> - </ErrorMessageHandler> + <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="institution-name" ariaLabel="Institution Name" isSubmitting={isSubmitting} + placeholder="Name" required={doEnforceRequiredFields()} + defaultValue={requestData.request.institutionname}/> + </div> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-10"> - <Title text="6. Co-Investigators"/> + <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="institution-city" ariaLabel="Institution City" isSubmitting={isSubmitting} + placeholder="City" required={doEnforceRequiredFields()} + defaultValue={requestData.request.institutioncity}/> + </div> - <CoInvestigators isSubmitting={isSubmitting} required={doEnforceRequiredFields()} coinvestigators={requestData.coinvestigators} onAddRecord={onAddInvestigator} onRemoveRecord={onRemoveCoInvestigator} /> - </div> + <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="institution-state" ariaLabel="Institution State" isSubmitting={isSubmitting} + placeholder="State" required={doEnforceRequiredFields()} + defaultValue={requestData.request.institutionstate}/> + </div> - <Title text="7. Existing or proposed funding source (select all that apply)"/> - {/* TODO: Make into checkbox group*/} - <Funding id="funding" isSubmitting={isSubmitting} defaultValue={requestData.request} required={doEnforceRequiredFields()}/> + <div className="tw-w-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="institution-country" ariaLabel="Institution Country" isSubmitting={isSubmitting} + placeholder="Country" required={doEnforceRequiredFields()} + defaultValue={requestData.request.institutioncountry}/> + </div> - <h3>Institutional Animal Facilities and Capabilities</h3> - <div className="tw-w-full tw-px-3"> - <Title text="1. Does your institution have existing NHP facilities?"/> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <Select id="existing-nhp-facilities" ariaLabel="Existing NHP Facilities" isSubmitting={isSubmitting} options={existingNHPFacilityOptions} defaultValue={requestData.request.existingnhpfacilities} required={doEnforceRequiredFields()}/> + <Title text="4. Affiliated Research Institution Type*"/> + + <div className="tw-full md:tw-w-1/3 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Select id="institution-type" ariaLabel="Institution Type" isSubmitting={isSubmitting} + placeholder="Type" required={doEnforceRequiredFields()} + defaultValue={requestData.request.institutiontype} + options={institutionTypeOptions}/> + </div> </div> </ErrorMessageHandler> - <Title text="2. Does your institution have an existing marmoset colony?"/> <ErrorMessageHandler isSubmitting={isSubmitting}> <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <Select id="existing-marmoset-colony" ariaLabel="Existing Marmoset Colony" isSubmitting={isSubmitting} options={existingMarmosetColonyOptions} defaultValue={requestData.request.existingmarmosetcolony} required={doEnforceRequiredFields()}/> + <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> + <Title text="5. Institution Signing Official* "/> + <Tooltip id="signing-official-helper" + text={signingOfficialTooltip} + /> + </div> + + + <div className="tw-flex tw-flex-wrap tw-mt-6"> + <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="official-last-name" ariaLabel="Last Name" isSubmitting={isSubmitting} + placeholder="Last Name" required={doEnforceRequiredFields()} + defaultValue={requestData.request.officiallastname}/> + </div> + + <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="official-first-name" ariaLabel="First Name" isSubmitting={isSubmitting} + placeholder="First Name" required={doEnforceRequiredFields()} + defaultValue={requestData.request.officialfirstname}/> + </div> + + <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="official-email" ariaLabel="Email Address" isSubmitting={isSubmitting} + placeholder="Email Address" required={doEnforceRequiredFields()} + defaultValue={requestData.request.officialemail}/> + </div> + </div> </div> </ErrorMessageHandler> - <Title text="3. Do you plan to breed marmosets?"/> - <div className="tw-w-full tw-px-3 tw-mb-4"> - <AnimalBreeding id="animal-breeding" isSubmitting={isSubmitting} request={requestData.request} required={doEnforceRequiredFields()}/> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-10"> + <Title text="6. Co-Investigators"/> + + <CoInvestigators isSubmitting={isSubmitting} required={doEnforceRequiredFields()} + coinvestigators={requestData.coinvestigators} onAddRecord={onAddInvestigator} + onRemoveRecord={onRemoveCoInvestigator}/> </div> - </div> - <h3>Research Details</h3> - - <div className="tw-flex tw-flex-wrap tw-mx-2"> - <div className="tw-w-full tw-px-3 tw-mb-4"> - <Title text={"1. " + experimentalRationalePlaceholder}/> - <Tooltip id="research-use-statement-helper" - text={experimentalRationalePlaceholder} - /> + <Title text="7. Existing or proposed funding source (select all that apply)"/> + {/* TODO: Make into checkbox group*/} + <Funding id="funding" isSubmitting={isSubmitting} defaultValue={requestData.request} + required={doEnforceRequiredFields()}/> + <h3>Institutional Animal Facilities and Capabilities</h3> + <div className="tw-w-full tw-px-3"> + <Title text="1. Does your institution have existing NHP facilities?"/> <ErrorMessageHandler isSubmitting={isSubmitting}> - <TextArea id="experiment-rationale" ariaLabel="Experimental rationale" isSubmitting={isSubmitting} placeholder={experimentalRationalePlaceholder} required={doEnforceRequiredFields()} defaultValue={requestData.request.experimentalrationale}/> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> + <Select id="existing-nhp-facilities" ariaLabel="Existing NHP Facilities" + isSubmitting={isSubmitting} options={existingNHPFacilityOptions} + defaultValue={requestData.request.existingnhpfacilities} + required={doEnforceRequiredFields()}/> + </div> </ErrorMessageHandler> - </div> - - <Title text="2. Animal Cohorts"/> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-6"> - <AnimalCohorts isSubmitting={isSubmitting} cohorts={requestData.cohorts} required={doEnforceRequiredFields()} onAddCohort={onAddCohort} onRemoveCohort={onRemoveCohort}/> - </div> - <Title text={"3. " + methodsProposedPlaceholder}/> - <div className="tw-w-full tw-px-3 tw-mb-6"> + <Title text="2. Does your institution have an existing marmoset colony?"/> <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-w-full tw-px-3 tw-mb-6"> - <TextArea id="methods-proposed" ariaLabel="Methods Proposed" isSubmitting={isSubmitting} placeholder={methodsProposedPlaceholder} required={doEnforceRequiredFields()} defaultValue={requestData.request.methodsproposed}/> - </div> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> + <Select id="existing-marmoset-colony" ariaLabel="Existing Marmoset Colony" + isSubmitting={isSubmitting} options={existingMarmosetColonyOptions} + defaultValue={requestData.request.existingmarmosetcolony} + required={doEnforceRequiredFields()}/> + </div> </ErrorMessageHandler> + + <Title text="3. Do you plan to breed marmosets?"/> + <div className="tw-w-full tw-px-3 tw-mb-4"> + <AnimalBreeding id="animal-breeding" isSubmitting={isSubmitting} request={requestData.request} + required={doEnforceRequiredFields()}/> + </div> </div> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> - <Title text={"4. " + terminalProceduresLabel}/> - </div> + <h3>Research Details</h3> + + <div className="tw-flex tw-flex-wrap tw-mx-2"> + <div className="tw-w-full tw-px-3 tw-mb-4"> + <Title text={'1. ' + experimentalRationalePlaceholder}/> + <Tooltip id="research-use-statement-helper" + text={experimentalRationalePlaceholder} + /> + + <ErrorMessageHandler isSubmitting={isSubmitting}> + <TextArea id="experiment-rationale" ariaLabel="Experimental rationale" + isSubmitting={isSubmitting} placeholder={experimentalRationalePlaceholder} + required={doEnforceRequiredFields()} + defaultValue={requestData.request.experimentalrationale}/> + </ErrorMessageHandler> + </div> - <div className="tw-w-full tw-px-3 tw-mt-6"> - <YesNoRadio id="is-terminalprocedures" ariaLabel="Terminal procedures" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} defaultValue={requestData.request.terminalprocedures}/> - </div> + <Title text="2. Animal Cohorts"/> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-6"> + <AnimalCohorts isSubmitting={isSubmitting} cohorts={requestData.cohorts} + required={doEnforceRequiredFields()} onAddCohort={onAddCohort} + onRemoveCohort={onRemoveCohort}/> </div> - </ErrorMessageHandler> - <Title text={"5. " + collaborationsPlaceholder}/> - <div className="tw-w-full tw-px-3 tw-mb-6"> - <ErrorMessageHandler isSubmitting={isSubmitting}> + <Title text={'3. ' + methodsProposedPlaceholder}/> <div className="tw-w-full tw-px-3 tw-mb-6"> - <TextArea id="collaborations" ariaLabel="Collaborations" isSubmitting={isSubmitting} placeholder={collaborationsPlaceholder} required={doEnforceRequiredFields()} defaultValue={requestData.request.collaborations}/> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-w-full tw-px-3 tw-mb-6"> + <TextArea id="methods-proposed" ariaLabel="Methods Proposed" isSubmitting={isSubmitting} + placeholder={methodsProposedPlaceholder} required={doEnforceRequiredFields()} + defaultValue={requestData.request.methodsproposed}/> + </div> + </ErrorMessageHandler> </div> - </ErrorMessageHandler> - </div> - <Title text={"6. " + animalWellfarePlaceholder}/> - <div className="tw-w-full tw-px-3 tw-mb-6"> <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> + <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> + <Title text={'4. ' + terminalProceduresLabel}/> + </div> + + <div className="tw-w-full tw-px-3 tw-mt-6"> + <YesNoRadio id="is-terminalprocedures" ariaLabel="Terminal procedures" + isSubmitting={isSubmitting} required={doEnforceRequiredFields()} + defaultValue={requestData.request.terminalprocedures}/> + </div> + </div> + </ErrorMessageHandler> + + <Title text={'5. ' + collaborationsPlaceholder}/> <div className="tw-w-full tw-px-3 tw-mb-6"> - <TextArea id="animal-welfare" ariaLabel="Animal Welfare" isSubmitting={isSubmitting} placeholder={animalWellfarePlaceholder} required={doEnforceRequiredFields()} defaultValue={requestData.request.animalwelfare}/> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-w-full tw-px-3 tw-mb-6"> + <TextArea id="collaborations" ariaLabel="Collaborations" isSubmitting={isSubmitting} + placeholder={collaborationsPlaceholder} required={doEnforceRequiredFields()} + defaultValue={requestData.request.collaborations}/> + </div> + </ErrorMessageHandler> </div> - </ErrorMessageHandler> - <ErrorMessageHandler isSubmitting={isSubmitting}> + <Title text={'6. ' + animalWellfarePlaceholder}/> <div className="tw-w-full tw-px-3 tw-mb-6"> - <input type="checkbox" name="certify" id="certify" aria-label="Certify" className={(isSubmitting ? "custom-invalid" : "")} required={doEnforceRequiredFields()} defaultChecked={requestData.request.certify}/> - <label className="tw-text-gray-700 ml-1">{certificationLabel}</label> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-w-full tw-px-3 tw-mb-6"> + <TextArea id="animal-welfare" ariaLabel="Animal Welfare" isSubmitting={isSubmitting} + placeholder={animalWellfarePlaceholder} required={doEnforceRequiredFields()} + defaultValue={requestData.request.animalwelfare}/> + </div> + </ErrorMessageHandler> + + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-w-full tw-px-3 tw-mb-6"> + <input type="checkbox" name="certify" id="certify" aria-label="Certify" + className={(isSubmitting ? 'custom-invalid' : '')} + required={doEnforceRequiredFields()} + defaultChecked={requestData.request.certify}/> + <label className="tw-text-gray-700 ml-1">{certificationLabel}</label> + </div> + </ErrorMessageHandler> </div> + + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> + <Title text="7. Attending veterinarian"/> + + <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="vet-last-name" ariaLabel="Last Name" isSubmitting={isSubmitting} + placeholder="Last Name" required={doEnforceRequiredFields()} + defaultValue={requestData.request.vetlastname}/> + </div> + + <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="vet-first-name" ariaLabel="First Name" isSubmitting={isSubmitting} + placeholder="First Name" required={doEnforceRequiredFields()} + defaultValue={requestData.request.vetfirstname}/> + </div> + + <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> + <Input id="vet-email" ariaLabel="Email" isSubmitting={isSubmitting} + placeholder="Email Address" required={doEnforceRequiredFields()} + defaultValue={requestData.request.vetemail}/> + </div> + </div> </ErrorMessageHandler> </div> - <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-flex tw-flex-wrap tw-mx-2 tw-mb-4"> - <Title text="7. Attending veterinarian"/> - - <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="vet-last-name" ariaLabel="Last Name" isSubmitting={isSubmitting} placeholder="Last Name" required={doEnforceRequiredFields()} defaultValue={requestData.request.vetlastname}/> - </div> - - <div className="tw-w-full md:tw-w-1/2 tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="vet-first-name" ariaLabel="First Name" isSubmitting={isSubmitting} placeholder="First Name" required={doEnforceRequiredFields()} defaultValue={requestData.request.vetfirstname}/> - </div> + <IACUCProtocol id="iacuc" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} + request={requestData.request}/> - <div className="tw-w-full tw-px-3 tw-mb-6 md:tw-mb-0"> - <Input id="vet-email" ariaLabel="Email" isSubmitting={isSubmitting} placeholder="Email Address" required={doEnforceRequiredFields()} defaultValue={requestData.request.vetemail}/> - </div> + <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> + <Title text="9. Will participate in the MCC Census? "/> + <Tooltip id="census-helper" text={censusToolTip}/> + </div> + <div className="tw-w-full tw-px-3 tw-mt-3"> + <AnimalCensus id="census" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} + request={requestData.request}/> </div> - </ErrorMessageHandler> - </div> - - <IACUCProtocol id="iacuc" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} request={requestData.request}/> - <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-0"> - <Title text="9. Will participate in the MCC Census? "/> - <Tooltip id="census-helper" text={censusToolTip}/> - </div> - <div className="tw-w-full tw-px-3 tw-mt-3"> - <AnimalCensus id="census" isSubmitting={isSubmitting} required={doEnforceRequiredFields()} request={requestData.request}/> - </div> + <div className="tw-relative tw-w-full tw-mb-6 md:tw-mb-4"> + <Title text="10. Shipment Acknowledgement "/> + </div> - <Title text={"10. " + commentsPlaceholder}/> - <div className="tw-w-full tw-px-3 tw-mb-6"> <ErrorMessageHandler isSubmitting={isSubmitting}> - <div className="tw-w-full tw-px-3 tw-mb-6"> - <TextArea id="comments" ariaLabel="Comments" isSubmitting={isSubmitting} placeholder={commentsPlaceholder} required={false} defaultValue={requestData.request.comments}/> + <div className="tw-w-full tw-px-6 tw-mb-6"> + {shippingAcknowledgementStatement} + <p /> + <input type="checkbox" name="shippingAcknowledgement" id="shippingAcknowledgement" aria-label="Shipping Acknowledgement" + className={(isSubmitting ? 'custom-invalid' : '')} + required={doEnforceRequiredFields()} + defaultChecked={requestData.request.shippingAcknowledgement}/> + <label className="tw-text-gray-700 ml-1">I acknowledge this statement</label> </div> </ErrorMessageHandler> - </div> - <div className="tw-flex tw-flex-wrap tw-mx-2"> - <Title text="Request Status: "/>{requestData.request.status} - </div> + <Title text={'11. ' + commentsPlaceholder}/> + <div className="tw-w-full tw-px-3 tw-mb-6"> + <ErrorMessageHandler isSubmitting={isSubmitting}> + <div className="tw-w-full tw-px-3 tw-mb-6"> + <TextArea id="comments" ariaLabel="Comments" isSubmitting={isSubmitting} + placeholder={commentsPlaceholder} required={false} + defaultValue={requestData.request.comments}/> + </div> + </ErrorMessageHandler> + </div> - <div className="tw-flex tw-flex-wrap tw-mx-2"> - <Button baseColor="red" marginLeft="auto" text="Cancel" onClick={(e) => { - e.preventDefault() + <div className="tw-flex tw-flex-wrap tw-mx-2"> + <Title text="Request Status: "/>{requestData.request.status} + </div> - if (confirm("You are about to leave this page.")) { - window.location.href = ActionURL.buildURL('mcc', 'mccRequests.view') - } - }} /> + <div className="tw-flex tw-flex-wrap tw-mx-2"> + <Button baseColor="red" marginLeft="auto" text="Cancel" onClick={(e) => { + e.preventDefault(); - <Button onClick={(e) => { - handleSubmitButton(e, false); - }} text={getSaveButtonText()} display={hasEditPermission()}/> + if (confirm('You are about to leave this page.')) { + window.location.href = ActionURL.buildURL('mcc', 'mccRequests.view'); + } + }}/> - <Button onClick={(e) => { - handleSubmitButton(e, true); - }} text={getSubmitButtonText()} display={hasEditPermission()}/> + <Button onClick={(e) => { + handleSubmitButton(e, false); + }} text={getSaveButtonText()} display={hasEditPermission()}/> - <Button onClick={(e) => { - e.preventDefault() - setShowWithdrawDialog(true) - }} text={"Withdraw"} display={shouldShowWithdraw()}/> - </div> - </form> - - <SavingOverlay display={displayOverlay} /> - - <Dialog open={showWithdrawDialog}> - <DialogTitle>Withdraw Request</DialogTitle> - <DialogContent> - <DialogContentText>Please enter a reason for withdrawing this request</DialogContentText> - <TextareaAutosize - minRows={4} - id="withdrawReason" - required={true} - autoFocus={true} - defaultValue={withdrawReasonText} - form={"animalRequestForm"} - onChange={(e) => setWithdrawReasonText(e.target.value)} - /> - </DialogContent> - <DialogActions> - <Box mr="5px"> <Button onClick={(e) => { - if (!withdrawReasonText) { + handleSubmitButton(e, true); + }} text={getSubmitButtonText()} display={hasEditPermission()}/> + + <Button onClick={(e) => { + e.preventDefault(); + setShowWithdrawDialog(true); + }} text={'Withdraw'} display={shouldShowWithdraw()}/> + </div> + </form> + + <SavingOverlay display={displayOverlay}/> + + <Dialog open={showWithdrawDialog}> + <DialogTitle>Withdraw Request</DialogTitle> + <DialogContent> + <DialogContentText>Please enter a reason for withdrawing this request</DialogContentText> + <TextareaAutosize + minRows={4} + id="withdrawReason" + required={true} + autoFocus={true} + defaultValue={withdrawReasonText} + form={'animalRequestForm'} + onChange={(e) => setWithdrawReasonText(e.target.value)} + /> + </DialogContent> + <DialogActions> + <Box mr="5px"> + <Button onClick={(e) => { + if (!withdrawReasonText) { alert("Must enter the reason") return } diff --git a/mcc/src/client/AnimalRequest/components/values.ts b/mcc/src/client/AnimalRequest/components/values.ts index 055ea750a..afb771376 100644 --- a/mcc/src/client/AnimalRequest/components/values.ts +++ b/mcc/src/client/AnimalRequest/components/values.ts @@ -14,6 +14,7 @@ export const animalWellfarePlaceholder = "Animal welfare (proposed care and use) export const censusReasonPlaceholder = "Reason for not participating" export const certificationLabel = "I certify and I have obtained approval for this study from my institution." +export const shippingAcknowledgementStatement = "I will be ready to receive animals within 60 days of approval, provided that they are available from a breeding center. I understand that failure to do so will result in per diem charges billed to me." export const terminalProceduresLabel = "Includes terminal procedures?" export const fundingSourceOptions = [ diff --git a/mcc/src/client/Dashboard/Dashboard.tsx b/mcc/src/client/Dashboard/Dashboard.tsx index fe1ba520d..f9a2ba14a 100644 --- a/mcc/src/client/Dashboard/Dashboard.tsx +++ b/mcc/src/client/Dashboard/Dashboard.tsx @@ -8,9 +8,9 @@ import PieChart from '../components/dashboard/PieChart'; import BarChart from '../components/dashboard/BarChart'; export function Dashboard() { - const [demographics, setDemographics] = useState(null); - const [living, setLiving] = useState(null); - const [u24Assigned, setu24Assigned] = useState(null); + const [demographics, setDemographics] = useState<[]>(null); + const [living, setLiving] = useState<[]>(null); + const [u24Assigned, setu24Assigned] = useState<[]>(null); const ctx = getServerContext().getModuleContext('mcc') || {}; const containerPath = ctx.MCCContainer || null; @@ -66,20 +66,20 @@ export function Dashboard() { <div className="panel-heading">Census</div> <div className="row"> <div className="panel-body count-panel-body"> - <div className="count-panel-text">{demographics.length}</div> + <div className="count-panel-text">{new Intl.NumberFormat("en-IN").format(demographics.length)}</div> <div className="small text-muted">Marmosets tracked by MCC</div> </div> </div> <div className="row mcc-col-centered"> <div className="col-md-3"> <div className="panel-body count-panel-body"> - <div className="count-panel-text-small">{living.length}</div> + <div className="count-panel-text-small">{new Intl.NumberFormat("en-IN").format(living.length)}</div> <div className="small text-muted text-center">Living</div> </div> </div> <div className="col-md-3"> <div className="panel-body count-panel-body"> - <div className="count-panel-text-small">{u24Assigned.length}</div> + <div className="count-panel-text-small">{new Intl.NumberFormat("en-IN").format(u24Assigned.length)}</div> <div className="small text-muted text-center">U24 Assigned</div> </div> </div> @@ -88,9 +88,9 @@ export function Dashboard() { </div> <div className="col-md-4"> <div className="panel panel-default"> - <div className="panel-heading">Center (All Animals)</div> + <div className="panel-heading">Center (Living Animals)</div> <div className="panel-body"> - <PieChart fieldName = "colony" demographics={demographics} cutout = "30%" /> + <PieChart fieldName = "colony" demographics={living} cutout = "30%" collapseBelow = {0.025} /> </div> </div> </div> diff --git a/mcc/src/client/GeneticsPlot/GeneticsPlot.tsx b/mcc/src/client/GeneticsPlot/GeneticsPlot.tsx index 031af83d2..6a7908211 100644 --- a/mcc/src/client/GeneticsPlot/GeneticsPlot.tsx +++ b/mcc/src/client/GeneticsPlot/GeneticsPlot.tsx @@ -5,21 +5,13 @@ import ScatterChart from './ScatterChart'; import { Box, Tab, Tabs } from '@mui/material'; import KinshipTable from './KinshipTable'; import { ErrorBoundary } from '../components/ErrorBoundary'; +import SequenceDataTable from './SequenceDataTable'; -function GenomeBrowser(props: {jbrowseId: any}) { - const { jbrowseId } = props; - - return ( - <div> - <a href={ActionURL.buildURL('jbrowse', 'jbrowse', null, {session: jbrowseId})}>Click here to view Marmoset SNP data in the genome browser</a> - </div> - ); -} - export function GeneticsPlot() { const [pcaData, setPcaData] = useState([]); const [kinshipData, setKinshipData] = useState([]); + const [sequenceData, setSequenceData] = useState([]); const [jbrowseId, setJBrowseId] = useState(null); const [value, setValue] = React.useState(0); @@ -83,6 +75,29 @@ export function GeneticsPlot() { }, scope: this }); + + Query.selectRows({ + containerPath: containerPath, + schemaName: 'study', + queryName: 'genomicDatasets', + columns: 'Id,datatype,sra_accession,total_reads,objectid', + success: function(results) { + setSequenceData(results.rows.map((row) => { + return({ + id: row.objectid, + Id: row.Id, + datatype: row.datatype, + sra_accession: row.sra_accession, + total_reads: row.total_reads + }) + })) + }, + failure: function(response) { + alert('There was an error loading data'); + console.log(response); + }, + scope: this + }); }, [] /* only run the effect on mount */); if (!containerPath) { @@ -111,37 +126,29 @@ export function GeneticsPlot() { Over the past few years, the MCC team has been working on extracting, sequencing and analyzing DNA from marmosets across the participating breeding centers. While we have deposited the raw sequence data for 578 marmosets on NCBI's Sequence Read Archive (SRA), we are excited to report that the MCC portal now - houses a call set with single nucleotide variants and short indels for over 800 individuals. - <p/> - The MCC genomic database is extensive, with each individual being genotype at millions of variants - across the genome. One way to summarize a large dataset can be done using Principal Component Analysis - (PCA). PCA is a technique used across disciplines (from astronomy to genomics) that reduces the - information in a multi-dimensional dataset to (fewer) principal components (PC) that retain overall - trends and patterns in the original data. Biologically, this could mean merging together two variants - that are always inherited together into just one PC, making the data easier to analyze while maintaining - its most important patterns. See the **Visualization with PCA** tab below. - <p/> - Although PCA is useful for broad-scale comparisons, it is not very useful when trying to distinguish - whether two individuals are siblings or first-cousins, for instance. For that, we have better statistics - that can describe the genetic relatedness between two individuals. We estimated genetic relatedness for - all pairs of individuals for which we have whole-genome data, and made these available under the - **Kinship** tab. There you will find the inferred relationships between pairs of individuals as well as - the calculated kinship coefficient, which is a quantitative measure of genetic relatedness - (see <a href="https://en.wikipedia.org/wiki/Coefficient_of_relationship#Kinship_coefficient">here</a> for more details). - <p/> - It is possible to explore the full MCC database of variants with a graphical interface by accessing the - **Genome Browser** tab. There you can, for example, visualize all the variants present in your gene of - interest by typing it's name in the search bar. - <p/> - The genetic analyses described here were performed by Karina Ray (ONPRC), Murillo Rodrigues (ONPRC), and + houses a call set with single nucleotide variants and short indels for over 800 individuals. The genetic analyses + described here were performed by Karina Ray (ONPRC), Murillo Rodrigues (ONPRC), and Ric del Rosario (Broad Institute). Please contact us at <a href="mailto:mcc@ohsu.edu">mcc@ohsu.edu</a> with any questions. + <p/> + { jbrowseId ? ( + <> + In addition to the information in the tabs below, you can use the MCC genome browser to view variants and/or search by gene: + <p/> + <ul> + <li> + <a style={{fontWeight: 'bold'}} href={ActionURL.buildURL('jbrowse', 'jbrowse', null, {session: jbrowseId})}>Click here to open the genome browser</a> + </li> + </ul> + </> + ) : null } </div> + <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Tabs value={value} onChange={handleChange} aria-label="basic tabs example"> <Tab label="Population Genetic Diversity" {...a11yProps(0)} /> <Tab label="Kinship" {...a11yProps(1)} /> - <Tab label="Genetic Variants" {...a11yProps(2)} hidden={jbrowseId == null}/> + <Tab label="Sequence Datasets" {...a11yProps(2)}/> </Tabs> </Box> <div className="row"> @@ -150,7 +157,7 @@ export function GeneticsPlot() { <div className="panel-body"> {value === 0 && <ScatterChart data={pcaData}/>} {value === 1 && <KinshipTable data={kinshipData}/>} - {value === 2 && <GenomeBrowser jbrowseId={jbrowseId}/>} + {value === 2 && <SequenceDataTable data={sequenceData}/>} </div> </div> </div> diff --git a/mcc/src/client/GeneticsPlot/KinshipTable.tsx b/mcc/src/client/GeneticsPlot/KinshipTable.tsx index ee4f316fa..a95052ce1 100644 --- a/mcc/src/client/GeneticsPlot/KinshipTable.tsx +++ b/mcc/src/client/GeneticsPlot/KinshipTable.tsx @@ -13,6 +13,16 @@ export default function KinshipTable(props: {data: any}) { ] return ( + <> + <div style={{paddingBottom: 20, maxWidth: 1000}}> + Although PCA is useful for broad-scale comparisons, it is not very useful when trying to distinguish + whether two individuals are siblings or first-cousins, for instance. For that, we have better statistics + that can describe the genetic relatedness between two individuals. We estimated genetic relatedness for + all pairs of individuals for which we have whole-genome data, shown in the table below. There you will + find the inferred relationships between pairs of individuals as well as the calculated kinship coefficient, + which is a quantitative measure of genetic relatedness + (see <a href="https://en.wikipedia.org/wiki/Coefficient_of_relationship#Kinship_coefficient">here</a> for more details). + </div> <DataGrid autoHeight={true} columns={columns} @@ -24,5 +34,6 @@ export default function KinshipTable(props: {data: any}) { paginationModel={pageModel} onPaginationModelChange={(model) => setPageModel(model)} /> + </> ); } \ No newline at end of file diff --git a/mcc/src/client/GeneticsPlot/ScatterChart.tsx b/mcc/src/client/GeneticsPlot/ScatterChart.tsx index 3ffd23e34..1439e3584 100644 --- a/mcc/src/client/GeneticsPlot/ScatterChart.tsx +++ b/mcc/src/client/GeneticsPlot/ScatterChart.tsx @@ -82,6 +82,17 @@ export default function ScatterChart(props: {data: any}) { } return ( + <> + <div style={{paddingBottom: 20, maxWidth: 1000}}> + The MCC genomic database is extensive, with each individual being genotype at millions of variants + across the genome. One way to summarize a large dataset can be done using Principal Component Analysis + (PCA). PCA is a technique used across disciplines (from astronomy to genomics) that reduces the + information in a multi-dimensional dataset to (fewer) principal components (PC) that retain overall + trends and patterns in the original data. Biologically, this could mean merging together two variants + that are always inherited together into just one PC, making the data easier to analyze while maintaining + its most important patterns. + </div> <Scatter data={chartData} options={chartOptions}/> + </> ); } \ No newline at end of file diff --git a/mcc/src/client/GeneticsPlot/SequenceDataTable.tsx b/mcc/src/client/GeneticsPlot/SequenceDataTable.tsx new file mode 100644 index 000000000..6ec8b2f35 --- /dev/null +++ b/mcc/src/client/GeneticsPlot/SequenceDataTable.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { DataGrid, GridColDef, GridPaginationModel, GridRenderCellParams, GridToolbar } from '@mui/x-data-grid'; + +export default function SequenceDataTable(props: {data: any}) { + const { data } = props; + const [pageModel, setPageModel] = React.useState<GridPaginationModel>({page: 0, pageSize: 25}); + + const columns: GridColDef[] = [ + { field: 'Id', headerName: 'Animal 1', width: 150, type: "string", headerAlign: 'left' }, + { field: 'datatype', headerName: 'Datatype', width: 250, type: "string", headerAlign: 'left' }, + { field: 'sra_accession', headerName: 'SRA Accession', width: 150, type: "string", headerAlign: 'right', renderCell: (params: GridRenderCellParams<any, string>) => { + return ( + <a + target="_blank" + href={params.value ? "https://trace.ncbi.nlm.nih.gov/Traces/sra/?run=" + params.value : ""} + >{params.value}</a> + ); + }}, + { field: 'total_reads', headerName: 'Total Reads', width: 125, type: "number", headerAlign: 'left', flex: 1 } + ] + + return ( + <> + <div style={{paddingBottom: 20, maxWidth: 1000}}> + </div> + <DataGrid + autoHeight={true} + columns={columns} + rows={data} + slots={{ + toolbar: GridToolbar + }} + pageSizeOptions={[10,25,50,100]} + paginationModel={pageModel} + onPaginationModelChange={(model) => setPageModel(model)} + /> + </> + ); +} \ No newline at end of file diff --git a/mcc/src/client/GeneticsPlot/webpart/app.tsx b/mcc/src/client/GeneticsPlot/webpart/app.tsx index 0d9235996..4b95f6c8f 100644 --- a/mcc/src/client/GeneticsPlot/webpart/app.tsx +++ b/mcc/src/client/GeneticsPlot/webpart/app.tsx @@ -4,6 +4,6 @@ import { App } from '@labkey/api'; import { GeneticsPlot } from '../GeneticsPlot'; -App.registerApp<any>('mccPcaWebpart', target => { +App.registerApp<any>('geneticsPlotWebpart', target => { ReactDOM.render(<GeneticsPlot />, document.getElementById(target)); }); diff --git a/mcc/src/client/GeneticsPlot/webpart/dev.tsx b/mcc/src/client/GeneticsPlot/webpart/dev.tsx index a503bfdbc..b49820176 100644 --- a/mcc/src/client/GeneticsPlot/webpart/dev.tsx +++ b/mcc/src/client/GeneticsPlot/webpart/dev.tsx @@ -4,6 +4,6 @@ import { App } from '@labkey/api'; import { GeneticsPlot } from '../GeneticsPlot'; -App.registerApp<any>('mccPcaWebpart', target => { +App.registerApp<any>('geneticsPlotWebpart', target => { ReactDOM.render(<GeneticsPlot />, document.getElementById(target)); }, true); \ No newline at end of file diff --git a/mcc/src/client/U24Dashboard/Dashboard.tsx b/mcc/src/client/U24Dashboard/Dashboard.tsx index 8819a0789..7ade780c7 100644 --- a/mcc/src/client/U24Dashboard/Dashboard.tsx +++ b/mcc/src/client/U24Dashboard/Dashboard.tsx @@ -8,14 +8,14 @@ import BarChart, { ColorType } from '../components/dashboard/BarChart'; import { ActiveElement, Chart, ChartEvent } from 'chart.js/dist/types/index'; export function Dashboard() { - const [demographics, setDemographics] = useState(null); - const [living, setLiving] = useState(null); - const [u24Assigned, setu24Assigned] = useState(null); - const [availableForTransfer, setAvailableForTransfer] = useState(null); - const [requestRows, setRequestRows] = useState(null); - const [censusRows, setCensusRows] = useState(null); - const [birthData, setBirthData ] = useState(null); - const [breedingPairData, setBreedingPairData ] = useState(null); + const [demographics, setDemographics] = useState<[]>(null); + const [living, setLiving] = useState<[]>(null); + const [u24Assigned, setu24Assigned] = useState<[]>(null); + const [availableForTransfer, setAvailableForTransfer] = useState<[]>(null); + const [requestRows, setRequestRows] = useState<[]>(null); + const [censusRows, setCensusRows] = useState<[]>(null); + const [birthData, setBirthData ] = useState<[]>(null); + const [breedingPairData, setBreedingPairData ] = useState<[]>(null); const ctx = getServerContext().getModuleContext('mcc') || {}; const containerPath = ctx.MCCContainer || null; @@ -147,20 +147,20 @@ export function Dashboard() { <div className="panel-heading">U24 Census</div> <div className="row"> <div className="panel-body count-panel-body"> - <div className="count-panel-text"><a href={ActionURL.buildURL("project", "begin", "", {pageId: "animalData"})}>{u24Assigned.length}</a></div> + <div className="count-panel-text"><a href={ActionURL.buildURL("project", "begin", "", {pageId: "animalData"})}>{new Intl.NumberFormat("en-IN").format(u24Assigned.length)}</a></div> <div className="small text-muted text-center">Total U24 Animals</div> </div> </div> <div className="row mcc-col-centered"> <div className="col-md-3"> <div className="panel-body count-panel-body"> - <div className="count-panel-text-small"><a href={ActionURL.buildURL("project", "begin", "", {pageId: "animalData", "u24.Availability~eq": "available for transfer"})}>{availableForTransfer.length}</a></div> + <div className="count-panel-text-small"><a href={ActionURL.buildURL("project", "begin", "", {pageId: "animalData", "u24.Availability~eq": "available for transfer"})}>{new Intl.NumberFormat("en-IN").format(availableForTransfer.length)}</a></div> <div className="small text-muted text-center">Available</div> </div> </div> <div className="col-md-3"> <div className="panel-body count-panel-body"> - <div className="count-panel-text-small"><a href={ActionURL.buildURL("project", "begin", "", {pageId: "requests"})}>{requestRows.length}</a></div> + <div className="count-panel-text-small"><a href={ActionURL.buildURL("project", "begin", "", {pageId: "requests"})}>{new Intl.NumberFormat("en-IN").format(requestRows.length)}</a></div> <div className="small text-muted text-center">Total Requests</div> </div> </div> diff --git a/mcc/src/client/components/RequestUtils.tsx b/mcc/src/client/components/RequestUtils.tsx index b25c4a8e6..4bca1478b 100644 --- a/mcc/src/client/components/RequestUtils.tsx +++ b/mcc/src/client/components/RequestUtils.tsx @@ -48,6 +48,7 @@ export class AnimalRequestProps { vetlastname: string; vetemail: string; vetfirstname: string; + shippingAcknowledgement: boolean; objectid: string; comments: string; } @@ -145,6 +146,7 @@ export async function queryRequestInformation(requestId, handleFailure) { "iacucprotocol", "grantnumber", "applicationduedate", + "shippingAcknowledgement", "comments", "status" ], diff --git a/mcc/src/client/components/dashboard/PieChart.tsx b/mcc/src/client/components/dashboard/PieChart.tsx index b9a28ec86..ca6880b4b 100644 --- a/mcc/src/client/components/dashboard/PieChart.tsx +++ b/mcc/src/client/components/dashboard/PieChart.tsx @@ -1,11 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { - Chart, - ArcElement, - Legend, - PieController, - Tooltip -} from 'chart.js'; +import { ArcElement, Chart, Legend, PieController, Tooltip } from 'chart.js'; Chart.register(ArcElement, Legend, PieController, Tooltip); @@ -21,14 +15,12 @@ const colors = [ "#999999" ]; -export default function PieChart(props) { +export default function PieChart(props: {demographics: [], fieldName: string, cutout?: string, collapseBelow?: number }) { const canvas = useRef(null); - const { demographics } = props; - const { fieldName } = props; - const { cutout } = props || 0; + const { demographics, fieldName, cutout = '0', collapseBelow = 0 } = props; - const collectedData = demographics.reduce((acc, curr) => { + const collectedData = demographics.reduce((acc, curr) => { const value = curr[fieldName] === null ? 'Unknown' : curr[fieldName]; if (acc[value]) { acc[value] = acc[value] + 1; @@ -37,7 +29,31 @@ export default function PieChart(props) { } return acc; - }, {}); + }, new Map<string, bigint>()) + + if (collapseBelow) { + const total = Object.keys(collectedData).reduce((sum, keyName) => { + sum += collectedData[keyName] + + return sum + }, 0) + + const otherValue = Object.keys(collectedData).reduce((sum, keyName) => { + const val = collectedData[keyName] + const fraction = val / total + if (fraction < collapseBelow) { + delete collectedData[keyName] + sum += val + } + + return sum + }, 0) + + if (otherValue) { + collectedData['Other'] = otherValue + } + } + const labels = Object.keys(collectedData).sort(Intl.Collator().compare); const data = labels.map(label => collectedData[label]); diff --git a/mcc/src/client/entryPoints.js b/mcc/src/client/entryPoints.js index 9063de69b..2f2cf21e4 100644 --- a/mcc/src/client/entryPoints.js +++ b/mcc/src/client/entryPoints.js @@ -24,12 +24,13 @@ module.exports = { name: 'geneticsPlot', title: 'Marmoset Genetics', permissionClasses: ['org.labkey.api.security.permissions.ReadPermission'], - path: './src/client/GeneticsPlot' + path: './src/client/GeneticsPlot', }, { name: 'geneticsPlotWebpart', title: 'Marmoset Genetics', permissionClasses: ['org.labkey.api.security.permissions.ReadPermission'], - path: './src/client/GeneticsPlot/webpart' + path: './src/client/GeneticsPlot/webpart', + generateLib: true },{ name: 'u24Dashboard', title: 'U24 Dashboard', diff --git a/mcc/src/org/labkey/mcc/MccModule.java b/mcc/src/org/labkey/mcc/MccModule.java index c52176612..11292760b 100644 --- a/mcc/src/org/labkey/mcc/MccModule.java +++ b/mcc/src/org/labkey/mcc/MccModule.java @@ -77,7 +77,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 20.018; + return 20.019; } @Override diff --git a/mcc/src/org/labkey/mcc/MccUserSchema.java b/mcc/src/org/labkey/mcc/MccUserSchema.java index 004814851..71912fc5d 100644 --- a/mcc/src/org/labkey/mcc/MccUserSchema.java +++ b/mcc/src/org/labkey/mcc/MccUserSchema.java @@ -271,6 +271,7 @@ private TableInfo getGenomicsQuery() " d.date,\n" + " d.datatype,\n" + " d.sra_accession,\n" + + " d.total_reads,\n" + " d.objectid,\n" + " d.container\n" + "\n" + diff --git a/mcc/src/org/labkey/mcc/etl/PopulateGeneticDataStep.java b/mcc/src/org/labkey/mcc/etl/PopulateGeneticDataStep.java index 9e27a7c4e..06feea72d 100644 --- a/mcc/src/org/labkey/mcc/etl/PopulateGeneticDataStep.java +++ b/mcc/src/org/labkey/mcc/etl/PopulateGeneticDataStep.java @@ -68,7 +68,7 @@ private void populateGeneticData(PipelineJob job) throws PipelineJobException { //first select all rows from remote table SelectRowsCommand sr = new SelectRowsCommand(MccSchema.NAME, "genomicDatasetsSource"); - sr.setColumns(Arrays.asList("Id", "date", "datatype", "sra_accession")); + sr.setColumns(Arrays.asList("Id", "date", "datatype", "sra_accession", "total_reads")); TableInfo aggregatedDemographics = QueryService.get().getUserSchema(job.getUser(), job.getContainer(), MccSchema.NAME).getTable("aggregatedDemographics"); @@ -104,6 +104,7 @@ private void populateGeneticData(PipelineJob job) throws PipelineJobException newRow.put("date", x.get("date")); newRow.put("datatype", x.get("datatype")); newRow.put("sra_accession", x.get("sra_accession")); + newRow.put("total_reads", x.get("total_reads")); toInsert.get(target).add(newRow); }); diff --git a/mcc/test/src/org/labkey/test/tests/mcc/MccTest.java b/mcc/test/src/org/labkey/test/tests/mcc/MccTest.java index 300e78234..9c5caf4f7 100644 --- a/mcc/test/src/org/labkey/test/tests/mcc/MccTest.java +++ b/mcc/test/src/org/labkey/test/tests/mcc/MccTest.java @@ -463,6 +463,7 @@ else if ("radio".equals(inputType)) new FormElement("existing-nhp-facilities", "existingnhpfacilities", "Existing NHP facilities").select("existing"), new FormElement("animal-welfare", "animalwelfare", "welfare").inputType("textarea"), new FormElement("certify", "certify", true).checkBox(), + new FormElement("shippingAcknowledgement", "shippingAcknowledgement", true).checkBox(), new FormElement("vet-last-name", "vetlastname", "vet last name"), new FormElement("vet-first-name", "vetfirstname", "vet first name"), new FormElement("vet-email", "vetemail", "vet@email.com"), diff --git a/primeseq/src/org/labkey/primeseq/etl/VerifyRowCount.java b/primeseq/src/org/labkey/primeseq/etl/VerifyRowCount.java index 95d0c2f2c..b1dcd8609 100644 --- a/primeseq/src/org/labkey/primeseq/etl/VerifyRowCount.java +++ b/primeseq/src/org/labkey/primeseq/etl/VerifyRowCount.java @@ -1,6 +1,7 @@ package org.labkey.primeseq.etl; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.labkey.api.collections.CaseInsensitiveHashMap; @@ -50,7 +51,8 @@ private enum Settings destSchema(true), destQuery(true), destColumn(true), - destAdditionalFilters(false); + destAdditionalFilters(false), + reportOnly(false); private final boolean _isRequired; @@ -106,6 +108,11 @@ public void setSettings(Map<String, String> settings) _settings.putAll(settings); } + private boolean isReportOnly() + { + return _settings.containsKey(Settings.reportOnly.name()) && Boolean.parseBoolean(_settings.get(Settings.reportOnly.name())); + } + private DataIntegrationService.RemoteConnection getRemoteDataSource(String name, Container c, Logger log) throws IllegalStateException { DataIntegrationService.RemoteConnection rc = DataIntegrationService.get().getRemoteConnection(name, c, log); @@ -259,7 +266,13 @@ private void verifyRows(PipelineJob job) throws PipelineJobException if (source != dest) { - job.getLogger().error("Row counts do not match (source: {}, dest: {})!", source, dest); + if (isReportOnly()) { + job.getLogger().info("Row counts do not match (source: {}, dest: {})!", source, dest); + } + else + { + job.getLogger().error("Row counts do not match (source: {}, dest: {})!", source, dest); + } } } } diff --git a/primeseq/src/org/labkey/primeseq/pipeline/SequenceJobResourceAllocator.java b/primeseq/src/org/labkey/primeseq/pipeline/SequenceJobResourceAllocator.java index c213266fd..b7921c6a3 100644 --- a/primeseq/src/org/labkey/primeseq/pipeline/SequenceJobResourceAllocator.java +++ b/primeseq/src/org/labkey/primeseq/pipeline/SequenceJobResourceAllocator.java @@ -96,6 +96,11 @@ private int getAlignerIndexMem(PipelineJob job) } } } + else if (job.getClass().getName().endsWith("ReferenceLibraryPipelineJob")) + { + // This almost always includes bwa-mem + return 72; + } return 36; } @@ -179,8 +184,8 @@ public Integer getMaxRequestMemory(PipelineJob job) if (isGeneticsTask(job)) { - job.getLogger().debug("setting memory to 72"); - return 72; + job.getLogger().debug("setting memory to 96"); + return 96; } if (isCacheAlignerIndexesTask(job))