diff --git a/SequenceAnalysis/build.gradle b/SequenceAnalysis/build.gradle index 11a551af7..a78794a17 100644 --- a/SequenceAnalysis/build.gradle +++ b/SequenceAnalysis/build.gradle @@ -44,6 +44,7 @@ dependencies { BuildUtils.addLabKeyDependency(project: project, config: "apiImplementation", depProjectPath: ":server:modules:LabDevKitModules:laboratory", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "apiImplementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile") + BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:DiscvrLabKeyModules:discvrcore", depProjectConfig: "apiJarFile") BuildUtils.addExternalDependency( project, new ExternalDependency( diff --git a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java index c7f842e4b..a5a70b813 100644 --- a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java +++ b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisController.java @@ -72,6 +72,7 @@ import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; +import org.labkey.api.discvrcore.annotation.UtilityAction; import org.labkey.api.exceptions.OptimisticConflictException; import org.labkey.api.exp.ExperimentException; import org.labkey.api.exp.api.DataType; @@ -427,7 +428,8 @@ public void addNavTrail(NavTree tree) tree.addChild("Analyze Alignments"); } } - + + @UtilityAction(label = "Find Orphan Files", description = "This will start a pipeline job that will inspect all files in this folder to identify potential orphan or otherwise unnecessary files") @RequiresPermission(ReadPermission.class) public static class FindOrphanFilesAction extends ConfirmAction { @@ -4955,6 +4957,7 @@ public void setOutputFileIds(Integer[] outputFileIds) } } + @UtilityAction(label = "Update ExpData Path", description = "This will update the DataFileUrl on the selected ExpData to the path provided") @RequiresSiteAdmin public static class UpdateExpDataPathAction extends ConfirmAction { @@ -5241,4 +5244,4 @@ public void setDataFileUrl(String dataFileUrl) _dataFileUrl = dataFileUrl; } } -} \ No newline at end of file +} diff --git a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisMaintenanceTask.java b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisMaintenanceTask.java index 57ebce8cb..128d3448a 100644 --- a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisMaintenanceTask.java +++ b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceAnalysisMaintenanceTask.java @@ -300,6 +300,7 @@ private void processContainer(Container c, Logger log) throws IOException, Pipel if (root != null && !root.isCloudRoot()) { //first sequences + log.debug("Inspecting sequences"); File sequenceDir = new File(root.getRootPath(), ".sequences"); TableInfo tableRefNtSequences = SequenceAnalysisSchema.getTable(SequenceAnalysisSchema.TABLE_REF_NT_SEQUENCES); TableSelector ntTs = new TableSelector(tableRefNtSequences, new SimpleFilter(FieldKey.fromString("container"), c.getId()), null); @@ -342,6 +343,7 @@ private void processContainer(Container c, Logger log) throws IOException, Pipel } //then libraries + log.debug("Inspecting genomes"); File libraryDir = SequenceAnalysisManager.get().getReferenceLibraryDir(c); if (libraryDir != null && libraryDir.exists()) { @@ -516,6 +518,7 @@ private void processContainer(Container c, Logger log) throws IOException, Pipel } //finally outputfiles + log.debug("Inspecting outputs"); TableInfo ti = SequenceAnalysisSchema.getTable(SequenceAnalysisSchema.TABLE_OUTPUTFILES); TableSelector ts = new TableSelector(ti, Collections.singleton("dataid"), new SimpleFilter(FieldKey.fromString("container"), c.getId()), null); Set expectedFileNames = new HashSet<>(); @@ -563,6 +566,8 @@ private void processContainer(Container c, Logger log) throws IOException, Pipel } } } + + log.debug("done"); } for (Container child : c.getChildren()) diff --git a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceProvider.java b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceProvider.java index 5e2881121..092902552 100644 --- a/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceProvider.java +++ b/SequenceAnalysis/src/org/labkey/sequenceanalysis/SequenceProvider.java @@ -217,30 +217,38 @@ public List getSubjectIdSummary(Container c, User u, String subjectId) @Override public List getTabbedReportItems(Container c, User u) { + if (!c.getActiveModules().contains(getOwningModule())) + { + return Collections.emptyList(); + } + List items = new ArrayList<>(); - NavItem owner = getDataNavItems(c, u).get(0); + NavItem owner = getReportItems(c, u).get(0); String category = "Sequence Data"; QueryCache cache = new QueryCache(); TabbedReportItem readsets = new QueryTabbedReportItem(cache, this, SequenceAnalysisSchema.SCHEMA_NAME, SequenceAnalysisSchema.TABLE_READSETS, "Sequence Readsets", category); readsets.setOwnerKey(owner.getPropertyManagerKey()); + readsets.setVisible(owner.isVisible(c, u)); items.add(readsets); TabbedReportItem analyses = new QueryTabbedReportItem(cache, this, SequenceAnalysisSchema.SCHEMA_NAME, SequenceAnalysisSchema.TABLE_ANALYSES, "Sequence Analyses", category); analyses.setSubjectIdFieldKey(FieldKey.fromString("readset/subjectid")); analyses.setSampleDateFieldKey(FieldKey.fromString("readset/sampledate")); - analyses.setAllProjectsFieldKey(FieldKey.fromString("readset/allProjectsPivot")); - analyses.setOverlappingProjectsFieldKey(FieldKey.fromString("readset/overlappingProjectsPivot")); + analyses.setKeyOverride("allProjectsFieldName", FieldKey.fromString("readset/allProjectsPivot")); + analyses.setKeyOverride("overlappingProjectsFieldName", FieldKey.fromString("readset/overlappingProjectsPivot")); analyses.setOwnerKey(owner.getPropertyManagerKey()); + analyses.setVisible(owner.isVisible(c, u)); items.add(analyses); TabbedReportItem outputs = new QueryTabbedReportItem(cache, this, SequenceAnalysisSchema.SCHEMA_NAME, SequenceAnalysisSchema.TABLE_OUTPUTFILES, "Sequence Outputs", category); outputs.setSubjectIdFieldKey(FieldKey.fromString("readset/subjectid")); outputs.setSampleDateFieldKey(FieldKey.fromString("readset/sampledate")); - outputs.setAllProjectsFieldKey(FieldKey.fromString("readset/allProjectsPivot")); - outputs.setOverlappingProjectsFieldKey(FieldKey.fromString("readset/overlappingProjectsPivot")); + outputs.setKeyOverride("allProjectsFieldName", FieldKey.fromString("readset/allProjectsPivot")); + outputs.setKeyOverride("overlappingProjectsFieldName", FieldKey.fromString("readset/overlappingProjectsPivot")); outputs.setOwnerKey(owner.getPropertyManagerKey()); + outputs.setVisible(owner.isVisible(c, u)); items.add(outputs); return items; diff --git a/SequenceAnalysis/src/org/labkey/sequenceanalysis/pipeline/OrphanFilePipelineJob.java b/SequenceAnalysis/src/org/labkey/sequenceanalysis/pipeline/OrphanFilePipelineJob.java index 61c87a9e3..a73f34d78 100644 --- a/SequenceAnalysis/src/org/labkey/sequenceanalysis/pipeline/OrphanFilePipelineJob.java +++ b/SequenceAnalysis/src/org/labkey/sequenceanalysis/pipeline/OrphanFilePipelineJob.java @@ -344,6 +344,8 @@ public void getOrphanFilesForContainer(Container c, User u, Set orphanFile return; } + + getJob().updateStatusForTask(); if (getJob().isCancelled()) { throw new CancelledException(); diff --git a/Studies/.gitignore b/Studies/.gitignore new file mode 100644 index 000000000..5c2a2366d --- /dev/null +++ b/Studies/.gitignore @@ -0,0 +1 @@ +resources/credits/jars.txt \ No newline at end of file diff --git a/Studies/api-src/org/labkey/api/studies/StudiesService.java b/Studies/api-src/org/labkey/api/studies/StudiesService.java index 249137680..848fde966 100644 --- a/Studies/api-src/org/labkey/api/studies/StudiesService.java +++ b/Studies/api-src/org/labkey/api/studies/StudiesService.java @@ -1,12 +1,15 @@ package org.labkey.api.studies; import org.labkey.api.data.Container; +import org.labkey.api.data.TableCustomizer; import org.labkey.api.module.Module; import org.labkey.api.resource.Resource; import org.labkey.api.security.User; +import org.labkey.api.studies.study.EventProvider; import org.labkey.api.util.Path; import java.io.IOException; +import java.util.List; /** * Created by bimber on 11/3/2016. @@ -28,4 +31,10 @@ static public void setInstance(StudiesService instance) abstract public void importFolderDefinition(Container container, User user, Module m, Path sourceFolderDirPath) throws IOException; abstract public void loadTsv(Resource tsv, String schemaName, User u, Container c); + + abstract public void registerEventProvider(EventProvider ep); + + abstract public List getEventProviders(Container c); + + abstract public TableCustomizer getStudiesTableCustomizer(); } diff --git a/Studies/src/org/labkey/studies/query/ResultsOOODisplayColumn.java b/Studies/api-src/org/labkey/api/studies/query/ResultsOORDisplayColumn.java similarity index 71% rename from Studies/src/org/labkey/studies/query/ResultsOOODisplayColumn.java rename to Studies/api-src/org/labkey/api/studies/query/ResultsOORDisplayColumn.java index 4fddcc0e3..51a136aa2 100644 --- a/Studies/src/org/labkey/studies/query/ResultsOOODisplayColumn.java +++ b/Studies/api-src/org/labkey/api/studies/query/ResultsOORDisplayColumn.java @@ -1,4 +1,4 @@ -package org.labkey.studies.query; +package org.labkey.api.studies.query; import org.apache.commons.lang3.StringUtils; import org.labkey.api.data.ColumnInfo; @@ -9,9 +9,9 @@ import java.text.DecimalFormat; import java.util.Set; -public class ResultsOOODisplayColumn extends DataColumn +public class ResultsOORDisplayColumn extends DataColumn { - public ResultsOOODisplayColumn(ColumnInfo col) + public ResultsOORDisplayColumn(ColumnInfo col) { super(col); } @@ -48,15 +48,15 @@ public Object getDisplayValue(RenderContext ctx) private FieldKey getOOR() { - FieldKey oor = FieldKey.fromString("resultOOOIndicator"); - if (getBoundColumn() != null) + ColumnInfo col = getBoundColumn(); + if (col == null) { - return FieldKey.fromParts(getBoundColumn().getFieldKey().getParent(), oor); - } - else - { - return oor; + return null; } + + FieldKey oor = FieldKey.fromString(col.getFieldKey().getName() + "OORIndicator"); + + return getBoundColumn().getFieldKey().getParent() == null ? oor : FieldKey.fromParts(getBoundColumn().getFieldKey().getParent(), oor); } @Override diff --git a/Studies/api-src/org/labkey/api/studies/study/AbstractEventProvider.java b/Studies/api-src/org/labkey/api/studies/study/AbstractEventProvider.java new file mode 100644 index 000000000..11eed6e32 --- /dev/null +++ b/Studies/api-src/org/labkey/api/studies/study/AbstractEventProvider.java @@ -0,0 +1,88 @@ +package org.labkey.api.studies.study; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.TableInfo; +import org.labkey.api.module.Module; +import org.labkey.api.query.QueryService; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.User; +import org.labkey.api.security.permissions.ReadPermission; + +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractEventProvider implements EventProvider +{ + private final String _name; + private final String _label; + private final String _description; + private final Module _owner; + + public AbstractEventProvider(String name, String label, String description, Module owner) + { + _name = name; + _label = label; + _description = description; + _owner = owner; + } + + @Override + public String getDescription() + { + return _description; + } + + @Override + public String getLabel() + { + return _label; + } + + @Override + public String getName() + { + return _name; + } + + @Override + public boolean isAvailable(Container c) + { + return c.getActiveModules().contains(_owner); + } + + @Override + public final Map inferDates(Collection subjectList, Container c, User u) + { + Map result = new HashMap<>(inferDatesRaw(subjectList, c, u)); + subjectList.forEach(x -> { + if (!result.containsKey(x)) + { + result.put(x, null); + } + }); + + return result; + } + + abstract protected Map inferDatesRaw(Collection subjectList, Container c, User u); + + protected @Nullable TableInfo getTable(Container c, User u, String schema, String table) + { + UserSchema us = QueryService.get().getUserSchema(u, c, schema); + if (us == null) + { + return null; + } + + TableInfo ti = us.getTable("assignment"); + if (ti == null || !ti.hasPermission(u, ReadPermission.class)) + { + return null; + } + + return ti; + } +} diff --git a/Studies/api-src/org/labkey/api/studies/study/EventProvider.java b/Studies/api-src/org/labkey/api/studies/study/EventProvider.java new file mode 100644 index 000000000..39690c242 --- /dev/null +++ b/Studies/api-src/org/labkey/api/studies/study/EventProvider.java @@ -0,0 +1,26 @@ +package org.labkey.api.studies.study; + +import org.labkey.api.data.Container; +import org.labkey.api.security.User; + +import java.util.Collection; +import java.util.Date; +import java.util.Map; + +/** + * Each study will have a handful of important dates, which are used to define relative dates for each subject/participant. + * The EventProvider classes provide a code-based way to establish the handful of critical dates. This code is executed to populate + * the KeyEvents table, which maps subject/event to date. + */ +public interface EventProvider +{ + boolean isAvailable(Container c); + + String getName(); + + String getLabel(); + + String getDescription(); + + Map inferDates(Collection subjectList, Container c, User u); +} diff --git a/Studies/build.gradle b/Studies/build.gradle index fcd61b0d7..d9b01ff3e 100644 --- a/Studies/build.gradle +++ b/Studies/build.gradle @@ -1,4 +1,5 @@ import org.labkey.gradle.util.BuildUtils; +import org.labkey.gradle.util.ExternalDependency; plugins { id 'org.labkey.build.module' @@ -6,6 +7,7 @@ plugins { dependencies { + BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:DiscvrLabKeyModules:discvrcore", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:laboratory", depProjectConfig: "apiJarFile") BuildUtils.addLabKeyDependency(project: project, config: "implementation", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "apiJarFile") @@ -13,5 +15,18 @@ dependencies BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: ":server:modules:LabDevKitModules:LDK", depProjectConfig: "published", depExtension: "module") BuildUtils.addLabKeyDependency(project: project, config: "modules", depProjectPath: ":server:modules:DiscvrLabKeyModules:discvrcore", depProjectConfig: "published", depExtension: "module") + + BuildUtils.addExternalDependency( + project, + new ExternalDependency( + "com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}", + "jackson-databind", + "jackson-databind", + "https://github.com/FasterXML/jackson-databind", + ExternalDependency.APACHE_2_LICENSE_NAME, + ExternalDependency.APACHE_2_LICENSE_URL, + "Parsing JSON Data" + ) + ) } diff --git a/Studies/resources/schemas/dbscripts/postgresql/studies-23.001-23.002.sql b/Studies/resources/schemas/dbscripts/postgresql/studies-23.001-23.002.sql new file mode 100644 index 000000000..7fb2a0551 --- /dev/null +++ b/Studies/resources/schemas/dbscripts/postgresql/studies-23.001-23.002.sql @@ -0,0 +1,90 @@ +CREATE TABLE studies.studies ( + rowid serial, + studyName varchar(1000), + label varchar(1000), + category varchar(1000), + description varchar(4000), + + lsid entityid, + container entityid, + created timestamp, + createdby int, + modified timestamp, + modifiedby int, + + CONSTRAINT PK_studies PRIMARY KEY (rowid) +); + +CREATE TABLE studies.studyCohorts ( + rowid serial, + studyId int, + cohortName varchar(4000), + label varchar(4000), + category varchar(4000), + description varchar(4000), + isControlGroup bool default false, + sortOrder int, + + lsid entityid, + container entityid, + created timestamp, + createdby int, + modified timestamp, + modifiedby int, + + CONSTRAINT PK_studyCohorts PRIMARY KEY (rowid) +); + +CREATE TABLE studies.anchorEvents ( + rowid serial, + studyId int, + label varchar(4000), + description varchar(4000), + eventProviderName varchar(1000), + + container entityid, + created timestamp, + createdby int, + modified timestamp, + modifiedby int, + + CONSTRAINT PK_anchorEvents PRIMARY KEY (rowid) +); + +CREATE TABLE studies.expectedTimepoints ( + rowid serial, + studyId int, + cohortId int, + label varchar(4000), + labelShort varchar(100), + description varchar(4000), + numericLabel int, + anchorEvent int, + rangeMin int, + rangeMax int, + + container entityid, + created timestamp, + createdby int, + modified timestamp, + modifiedby int, + + CONSTRAINT PK_expectedTimepoints PRIMARY KEY (rowid) +); + +CREATE TABLE studies.timepointToDate ( + rowid serial, + subjectId varchar(4000), + timepointId int, + dateMin timestamp, + dateMax timestamp, + isManualOverride bool default false, + + container entityid, + created timestamp, + createdby int, + modified timestamp, + modifiedby int, + + CONSTRAINT PK_timepointToDate PRIMARY KEY (rowid) +); \ No newline at end of file diff --git a/Studies/resources/schemas/dbscripts/postgresql/studies-23.002-23.003.sql b/Studies/resources/schemas/dbscripts/postgresql/studies-23.002-23.003.sql new file mode 100644 index 000000000..e9f37d2e0 --- /dev/null +++ b/Studies/resources/schemas/dbscripts/postgresql/studies-23.002-23.003.sql @@ -0,0 +1,15 @@ +CREATE TABLE studies.subjectAnchorDates ( + rowid serial, + subjectId varchar(4000), + date int, + eventLabel varchar(1000), + anchorEventId int, + + container entityid, + created timestamp, + createdby int, + modified timestamp, + modifiedby int, + + CONSTRAINT PK_subjectAnchorDates PRIMARY KEY (rowid) +); \ No newline at end of file diff --git a/Studies/resources/schemas/dbscripts/postgresql/studies-23.003-23.004.sql b/Studies/resources/schemas/dbscripts/postgresql/studies-23.003-23.004.sql new file mode 100644 index 000000000..a9287e26b --- /dev/null +++ b/Studies/resources/schemas/dbscripts/postgresql/studies-23.003-23.004.sql @@ -0,0 +1,2 @@ +ALTER TABLE studies.subjectAnchorDates DROP COLUMN date; +ALTER TABLE studies.subjectAnchorDates ADD COLUMN date timestamp; diff --git a/Studies/resources/schemas/dbscripts/postgresql/studies-23.004-23.005.sql b/Studies/resources/schemas/dbscripts/postgresql/studies-23.004-23.005.sql new file mode 100644 index 000000000..22642f048 --- /dev/null +++ b/Studies/resources/schemas/dbscripts/postgresql/studies-23.004-23.005.sql @@ -0,0 +1 @@ +ALTER TABLE studies.subjectAnchorDates ADD COLUMN dataSource varchar(1000); diff --git a/Studies/resources/schemas/dbscripts/sqlserver/studies-23.001-23.002.sql b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.001-23.002.sql new file mode 100644 index 000000000..5d7d6b54d --- /dev/null +++ b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.001-23.002.sql @@ -0,0 +1,90 @@ +CREATE TABLE studies.studies ( + rowid int identity(1,1), + studyName varchar(1000), + label varchar(1000), + category varchar(1000), + description varchar(4000), + + lsid entityid, + container entityid, + created datetime, + createdby int, + modified datetime, + modifiedby int, + + CONSTRAINT PK_studies PRIMARY KEY (rowid) +); + +CREATE TABLE studies.studyCohorts ( + rowid int identity(1,1), + studyId int, + cohortName varchar(4000), + label varchar(4000), + category varchar(4000), + description varchar(4000), + isControlGroup bit default 0, + sortOrder int, + + lsid entityid, + container entityid, + created datetime, + createdby int, + modified datetime, + modifiedby int, + + CONSTRAINT PK_studyCohorts PRIMARY KEY (rowid) +); + +CREATE TABLE studies.anchorEvents ( + rowid int identity(1,1), + studyId int, + label varchar(4000), + description varchar(4000), + eventProviderName varchar(1000), + + container entityid, + created datetime, + createdby int, + modified datetime, + modifiedby int, + + CONSTRAINT PK_anchorEvents PRIMARY KEY (rowid) +); + +CREATE TABLE studies.expectedTimepoints ( + rowid int identity(1,1), + studyId int, + cohortId int, + label varchar(4000), + labelShort varchar(100), + description varchar(4000), + numericLabel int, + anchorEvent int, + rangeMin int, + rangeMax int, + + container entityid, + created datetime, + createdby int, + modified datetime, + modifiedby int, + + CONSTRAINT PK_expectedTimepoints PRIMARY KEY (rowid) +); + +CREATE TABLE studies.timepointToDate ( + rowid int identity(1,1), + subjectId varchar(4000), + timepointId int, + dateMin datetime, + dateMax datetime, + isManualOverride bit default 0, + + container entityid, + created datetime, + createdby int, + modified datetime, + modifiedby int, + + CONSTRAINT PK_timepointToDate PRIMARY KEY (rowid) +); \ No newline at end of file diff --git a/Studies/resources/schemas/dbscripts/sqlserver/studies-23.002-23.003.sql b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.002-23.003.sql new file mode 100644 index 000000000..e4da213bd --- /dev/null +++ b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.002-23.003.sql @@ -0,0 +1,15 @@ +CREATE TABLE studies.subjectAnchorDates ( + rowid int identity(1,1), + subjectId varchar(4000), + date int, + eventLabel varchar(1000), + anchorEventId int, + + container entityid, + created datetime, + createdby int, + modified datetime, + modifiedby int, + + CONSTRAINT PK_subjectAnchorDates PRIMARY KEY (rowid) +); \ No newline at end of file diff --git a/Studies/resources/schemas/dbscripts/sqlserver/studies-23.003-23.004.sql b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.003-23.004.sql new file mode 100644 index 000000000..7d620921d --- /dev/null +++ b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.003-23.004.sql @@ -0,0 +1,2 @@ +ALTER TABLE studies.subjectAnchorDates DROP COLUMN date; +ALTER TABLE studies.subjectAnchorDates ADD date datetime; diff --git a/Studies/resources/schemas/dbscripts/sqlserver/studies-23.004-23.005.sql b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.004-23.005.sql new file mode 100644 index 000000000..8ed4561af --- /dev/null +++ b/Studies/resources/schemas/dbscripts/sqlserver/studies-23.004-23.005.sql @@ -0,0 +1 @@ +ALTER TABLE studies.subjectAnchorDates ADD dataSource varchar(1000); diff --git a/Studies/resources/schemas/studies.xml b/Studies/resources/schemas/studies.xml index 054cd9393..fd0d0a189 100644 --- a/Studies/resources/schemas/studies.xml +++ b/Studies/resources/schemas/studies.xml @@ -124,4 +124,434 @@ + + + + Studies + /studies/manageStudy.view?studyId=${rowid} + DETAILED + + + true + false + false + true + + + Study Name + + + Label + + + Category + + + Description + textarea + + + lsidtype + true + true + false + + ObjectUri + Object + exp + + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + +
+ + + + Study Cohorts + DETAILED + + + true + false + false + true + + + Study ID + + studies + studies + rowId + label + + + + Cohort Name + + + Label + + + Category + + + Description + textarea + + + Is Control Group? + + + Sort Order + + + lsidtype + true + true + false + + ObjectUri + Object + exp + + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + +
+ + + Study Anchor Events + + This table contains the key event types that anchor relative dates in this study + DETAILED + + + true + false + false + true + + + Study ID + + studies + studies + rowId + label + + + + Label + + + Description + textarea + + + Event Provider + + studies + studyEventTypes + name + label + + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + +
+ + + Study Expected Timepoints + + This table contains the expected timepoints within this study, by cohort + DETAILED + + + true + false + false + true + + + Study ID + + studies + studies + rowId + label + + + + Cohort ID + + studies + studyCohorts + rowId + label + + + + Label + + + Short Label + + + Description + textarea + + + Numeric Label + + + Anchor Event + + studies + anchorEvents + rowId + label + + + + Min Days Relative to Anchor + + + Max Days Relative to Anchor + + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + +
+ + + Timepoint to Date Range + + Per subject, this table contains the allowable date ranges to map record to timepoint label. It is usually populated automatically, but can be manually overridden + DETAILED + + + + + + + true + false + false + true + + + Subject Id + http://cpas.labkey.com/Study#ParticipantId + + + Timepoint + + studies + expectedTimepoints + rowId + label + + + + Min Allowable Date + + + Max Allowable Date + + + Is Manual Override? + + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + +
+ + + Timepoint to Date Range + + Per subject, this table contains the anchor dates, which are key dates that establish relative time within the study + DETAILED + + + + + + + true + false + false + true + + + Subject Id + http://cpas.labkey.com/Study#ParticipantId + false + + + Date + false + + + Event Label + + + Anchor Event Id + + studies + anchorEvents + rowId + label + + + + Data Source + + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + + false + true + true + + +
\ No newline at end of file diff --git a/Studies/resources/views/manageStudy.html b/Studies/resources/views/manageStudy.html new file mode 100644 index 000000000..ba72f5797 --- /dev/null +++ b/Studies/resources/views/manageStudy.html @@ -0,0 +1,59 @@ + + + +PLACEHOLDER: Make a page that accepts a studyId and renders useful UI to manage timepoints, run QC, and show data +

\ No newline at end of file diff --git a/Studies/resources/views/studiesDetails.html b/Studies/resources/views/studiesDetails.html new file mode 100644 index 000000000..861748943 --- /dev/null +++ b/Studies/resources/views/studiesDetails.html @@ -0,0 +1,9 @@ +PLACEHOLDER: make a page that accepts a studyId and renders useful detail, including: + +studies.studies +studies.studyCohorts +studies.expectedTimepoints + +summary of data, including total subjects, links to datasets + +links to show subject/timepoint mapping. Maybe link to a QC report. diff --git a/Studies/resources/views/studiesOverview.html b/Studies/resources/views/studiesOverview.html new file mode 100644 index 000000000..f257fc184 --- /dev/null +++ b/Studies/resources/views/studiesOverview.html @@ -0,0 +1,65 @@ + + diff --git a/Studies/resources/views/studiesOverview.view.xml b/Studies/resources/views/studiesOverview.view.xml new file mode 100644 index 000000000..40129791a --- /dev/null +++ b/Studies/resources/views/studiesOverview.view.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Studies/resources/views/studiesOverview.webpart.xml b/Studies/resources/views/studiesOverview.webpart.xml new file mode 100644 index 000000000..8821d658e --- /dev/null +++ b/Studies/resources/views/studiesOverview.webpart.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Studies/resources/web/studies/panel/StudiesFilterType.js b/Studies/resources/web/studies/panel/StudiesFilterType.js new file mode 100644 index 000000000..ede4ebf09 --- /dev/null +++ b/Studies/resources/web/studies/panel/StudiesFilterType.js @@ -0,0 +1,127 @@ +Ext4.define('Laboratory.panel.StudiesFilterType', { + extend: 'LDK.panel.AbstractFilterType', + alias: 'widget.studies-filtertype', + + statics: { + filterName: 'study', + label: 'Studies' + }, + + initComponent: function(){ + this.items = this.getItems(); + + this.callParent(); + }, + + getItems: function(){ + var ctx = this.filterContext; + var toAdd = []; + + toAdd.push({ + width: 200, + html: 'Choose Study:', + style: 'margin-bottom:10px' + }); + + toAdd.push({ + xtype: 'panel', + items: [{ + xtype: 'combo', + width: 265, + itemId: 'studyField', + displayField: 'studyName', + valueField: 'studyName', + multiSelect: false, + store: { + type: 'labkey-store', + schemaName: 'studies', + queryName: 'studies', + autoLoad: true + }, + value: Ext4.isArray(ctx.studies) ? ctx.studies.join(';') : ctx.studies + }] + }); + + return toAdd; + }, + + getFilters: function(){ + return { + studies: this.getStudies() + } + }, + + getFilterArray: function(tab){ + var filterArray = { + removable: [], + nonRemovable: [] + }; + + if (this.reportQCStates?.length) { + filterArray.nonRemovable.push(LABKEY.Filter.create('qcstate/label', this.reportQCStates, LABKEY.Filter.Types.EQUALS_ONE_OF)); + } + + var filters = this.getFilters(); + var report = tab.report; + var studyFieldName = report.additionalFieldKeys?.studyAssignmentFieldKey; + if (!studyFieldName){ + LDK.Utils.logToServer({ + message: 'A TabbedReport is attempting to load a study filter when it should have been stopped upstream', + level: 'ERROR', + includeContext: true + }); + + return filterArray; + } + + var studyName = filters.studies[0]; + filterArray.nonRemovable.push(LABKEY.Filter.create(studyFieldName, studyName, LABKEY.Filter.Types.CONTAINS)); + + return filterArray; + }, + + isValid: function(){ + var val = this.down('#studyField').getValue(); + if (!val || !val.length){ + return false; + } + + return true; + }, + + getFilterInvalidMessage: function(){ + return 'Error: Must choose a study'; + }, + + validateReportForFilterType: function(report){ + if (!report.additionalFieldKeys?.studyAssignmentFieldKey){ + return 'This report cannot be used with the selected filter type, because the report does not contain a field with study assignment information'; + } + + return null; + }, + + getTitle: function(){ + var studies = this.getStudies(); + + if (studies && studies.length){ + return studies.join(', '); + } + + return ''; + }, + + getStudies: function(){ + var projectArray = this.down('#studyField').getValue(); + if (projectArray && !Ext4.isArray(projectArray)) { + projectArray = [projectArray]; + } + + if (projectArray && projectArray.length > 0){ + projectArray = Ext4.unique(projectArray); + projectArray.sort(); + } + + return projectArray; + } +}); \ No newline at end of file diff --git a/Studies/src/org/labkey/studies/StudiesModule.java b/Studies/src/org/labkey/studies/StudiesModule.java index abb00167c..f68d3746d 100644 --- a/Studies/src/org/labkey/studies/StudiesModule.java +++ b/Studies/src/org/labkey/studies/StudiesModule.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; +import org.labkey.api.laboratory.LaboratoryService; import org.labkey.api.ldk.ExtendedSimpleModule; import org.labkey.api.module.Module; import org.labkey.api.module.ModuleContext; @@ -10,8 +11,10 @@ import org.labkey.api.query.QuerySchema; import org.labkey.api.security.roles.RoleManager; import org.labkey.api.studies.StudiesService; -import org.labkey.studies.query.StudiesUserSchema; import org.labkey.api.studies.security.StudiesDataAdminRole; +import org.labkey.studies.query.StudiesUserSchema; +import org.labkey.studies.study.StudiesFilterProvider; +import org.labkey.studies.study.StudyEnrollmentEventProvider; import java.util.Collection; import java.util.Collections; @@ -30,7 +33,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 23.001; + return 23.005; } @Override @@ -39,7 +42,9 @@ protected void init() addController(StudiesController.NAME, StudiesController.class); StudiesService.setInstance(StudiesServiceImpl.get()); + StudiesService.get().registerEventProvider(new StudyEnrollmentEventProvider()); RoleManager.registerRole(new StudiesDataAdminRole()); + LaboratoryService.get().registerTabbedReportFilterProvider(new StudiesFilterProvider()); } @Override diff --git a/Studies/src/org/labkey/studies/StudiesSchema.java b/Studies/src/org/labkey/studies/StudiesSchema.java index 7e049f918..c1135e642 100644 --- a/Studies/src/org/labkey/studies/StudiesSchema.java +++ b/Studies/src/org/labkey/studies/StudiesSchema.java @@ -9,6 +9,12 @@ public class StudiesSchema private static final StudiesSchema _instance = new StudiesSchema(); public static final String NAME = "studies"; + public static final String TABLE_STUDIES = "studies"; + public static final String TABLE_COHORTS = "studyCohorts"; + public static final String TABLE_ANCHOR_EVENTS = "anchorEvents"; + public static final String TABLE_EXPECTED_TIMEPOINTS = "expectedTimepoints"; + public static final String TABLE_TIMEPOINT_TO_DATE = "timepointToDate"; + public static StudiesSchema getInstance() { return _instance; diff --git a/Studies/src/org/labkey/studies/StudiesServiceImpl.java b/Studies/src/org/labkey/studies/StudiesServiceImpl.java index 9edb5f9a9..4c3910b5b 100644 --- a/Studies/src/org/labkey/studies/StudiesServiceImpl.java +++ b/Studies/src/org/labkey/studies/StudiesServiceImpl.java @@ -3,6 +3,7 @@ import org.apache.logging.log4j.Logger; import org.labkey.api.admin.ImportOptions; import org.labkey.api.data.Container; +import org.labkey.api.data.TableCustomizer; import org.labkey.api.data.TableInfo; import org.labkey.api.module.Module; import org.labkey.api.pipeline.PipeRoot; @@ -18,9 +19,12 @@ import org.labkey.api.resource.Resource; import org.labkey.api.security.User; import org.labkey.api.studies.StudiesService; +import org.labkey.api.studies.study.EventProvider; +import org.labkey.api.util.ConfigurationException; import org.labkey.api.util.FileUtil; import org.labkey.api.util.Path; import org.labkey.api.util.logging.LogHelper; +import org.labkey.studies.query.StudiesTableCustomizer; import java.io.FileNotFoundException; import java.io.IOException; @@ -28,6 +32,7 @@ import java.io.OutputStream; import java.nio.file.Files; import java.sql.SQLException; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -120,7 +125,12 @@ public void loadTsv(Resource tsv, String schemaName, User u, Container c) qus.setBulkLoad(true); qus.truncateRows(u, c, null, null); - qus.insertRows(u, c, rows, new BatchValidationException(), null, null); + BatchValidationException bve = new BatchValidationException(); + qus.insertRows(u, c, rows, bve, null, null); + if (bve.hasErrors()) + { + throw bve; + } } catch (IOException | SQLException | BatchValidationException | QueryUpdateServiceException | DuplicateKeyException e) @@ -130,4 +140,29 @@ public void loadTsv(Resource tsv, String schemaName, User u, Container c) throw new RuntimeException(e); } } + + private final Map _eventProviders = new HashMap<>(); + + @Override + public void registerEventProvider(EventProvider ep) + { + if (_eventProviders.containsKey(ep.getName())) + { + throw new ConfigurationException("There is already a provider registered with the name: " + ep.getName()); + } + + _eventProviders.put(ep.getName(), ep); + } + + @Override + public List getEventProviders(Container c) + { + return _eventProviders.values().stream().filter(ep -> ep.isAvailable(c)).toList(); + } + + @Override + public TableCustomizer getStudiesTableCustomizer() + { + return new StudiesTableCustomizer(); + } } diff --git a/Studies/src/org/labkey/studies/query/StudiesTableCustomizer.java b/Studies/src/org/labkey/studies/query/StudiesTableCustomizer.java new file mode 100644 index 000000000..9b360fe1b --- /dev/null +++ b/Studies/src/org/labkey/studies/query/StudiesTableCustomizer.java @@ -0,0 +1,65 @@ +package org.labkey.studies.query; + +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.logging.log4j.Logger; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.collections.CaseInsensitiveKeyedHashSetValuedMap; +import org.labkey.api.data.AbstractTableInfo; +import org.labkey.api.data.TableCustomizer; +import org.labkey.api.data.TableInfo; +import org.labkey.api.ldk.LDKService; +import org.labkey.api.study.DatasetTable; +import org.labkey.api.util.logging.LogHelper; + +public class StudiesTableCustomizer implements TableCustomizer +{ + private static final Logger _log = LogHelper.getLogger(StudiesTableCustomizer.class, "Messages from StudiesTableCustomizer"); + + @Override + public void customize(TableInfo tableInfo) + { + MultiValuedMap props = new CaseInsensitiveKeyedHashSetValuedMap<>(); + if (tableInfo.getPkColumnNames().size() > 1) + { + final CaseInsensitiveHashMap keys = new CaseInsensitiveHashMap<>(); + tableInfo.getPkColumnNames().forEach(x -> keys.put(x, x)); + + if (keys.containsKey("objectId")) + { + props.put("primaryKeyField", keys.get("objectId")); + } + else if (tableInfo instanceof DatasetTable ds) + { + if (ds.getDataset().isDemographicData()) + { + String subjectCol = ds.getDataset().getStudy().getSubjectColumnName(); + if (keys.containsKey(subjectCol)) + { + props.put("primaryKeyField", keys.get(subjectCol)); + } + else + { + _log.error("Demographics dataset does not list subject col (" + subjectCol + ") as a PK. Table: " + tableInfo.getName()); + } + } + } + } + + LDKService.get().getDefaultTableCustomizer(props).customize(tableInfo); + if (tableInfo instanceof AbstractTableInfo ati) + { + doCustomize(ati); + } + else + { + _log.error("Expected table to be instance of AbstractTableInfo. Table: " + tableInfo.getName()); + } + } + + private void doCustomize(AbstractTableInfo ati) + { + // TODO: + // Overlapping studies/cohorts + // TimepointLabel + } +} diff --git a/Studies/src/org/labkey/studies/query/StudiesTriggerFactory.java b/Studies/src/org/labkey/studies/query/StudiesTriggerFactory.java new file mode 100644 index 000000000..b8805dd35 --- /dev/null +++ b/Studies/src/org/labkey/studies/query/StudiesTriggerFactory.java @@ -0,0 +1,89 @@ +package org.labkey.studies.query; + +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +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.FieldKey; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.studies.StudiesSchema; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class StudiesTriggerFactory implements TriggerFactory +{ + @Override + public @NotNull Collection createTrigger(@Nullable Container c, TableInfo table, Map extraContext) + { + return List.of(new StudyTrigger()); + } + + public static class StudyTrigger implements Trigger + { + @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 beforeInsert(TableInfo table, Container c, User user, @Nullable Map newRow, ValidationException errors, Map extraContext, @Nullable Map existingRecord) throws ValidationException + { + possiblyResolveStudy(newRow, c); + } + + @Override + public void beforeUpdate(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + { + possiblyResolveStudy(newRow, c); + } + + /** + * This allows incoming data to specify the study using the string name, which is resolved into the rowId + */ + private void possiblyResolveStudy(@Nullable Map row, Container c) + { + if (row == null) + { + return; + } + + possiblyResolveStudy(row, c, "studyId"); + if (row.get("studyId") == null & row.get("studyName") != null) + { + possiblyResolveStudy(row, c, "studyName"); + } + } + + private void possiblyResolveStudy(@Nullable Map row, Container c, String sourceProperty) + { + if (row == null) + { + return; + } + + if (row.get(sourceProperty) != null & row.get(sourceProperty) instanceof String) + { + if (!NumberUtils.isCreatable(row.get(sourceProperty).toString())) + { + Container target = c.isWorkbookOrTab() ? c.getParent() : c; + SimpleFilter filter = new SimpleFilter(FieldKey.fromString("container"), target.getEntityId()).addCondition(FieldKey.fromString("studyName"), row.get(sourceProperty)); + List rowIds = new TableSelector(StudiesSchema.getInstance().getSchema().getTable(StudiesSchema.TABLE_STUDIES), PageFlowUtil.set("rowId"), filter, null).getArrayList(Integer.class); + if (rowIds.size() == 1) + { + row.put("studyId", rowIds.get(0)); + } + } + } + } + } +} diff --git a/Studies/src/org/labkey/studies/query/StudiesUserSchema.java b/Studies/src/org/labkey/studies/query/StudiesUserSchema.java index 2fb451c26..faf061aab 100644 --- a/Studies/src/org/labkey/studies/query/StudiesUserSchema.java +++ b/Studies/src/org/labkey/studies/query/StudiesUserSchema.java @@ -1,7 +1,9 @@ package org.labkey.studies.query; +import org.apache.logging.log4j.Logger; import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.api.collections.CaseInsensitiveTreeSet; +import org.labkey.api.data.AbstractTableInfo; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.DbSchema; @@ -12,24 +14,39 @@ import org.labkey.api.ldk.table.ContainerScopedTable; import org.labkey.api.ldk.table.CustomPermissionsTable; import org.labkey.api.query.FieldKey; +import org.labkey.api.query.QueryDefinition; +import org.labkey.api.query.QueryException; +import org.labkey.api.query.QueryService; import org.labkey.api.query.SimpleUserSchema; import org.labkey.api.security.User; import org.labkey.api.security.permissions.DeletePermission; import org.labkey.api.security.permissions.InsertPermission; import org.labkey.api.security.permissions.ReadPermission; import org.labkey.api.security.permissions.UpdatePermission; -import org.labkey.studies.StudiesSchema; +import org.labkey.api.studies.StudiesService; import org.labkey.api.studies.security.StudiesDataAdminPermission; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.studies.StudiesSchema; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; +import static org.labkey.studies.StudiesSchema.TABLE_ANCHOR_EVENTS; +import static org.labkey.studies.StudiesSchema.TABLE_COHORTS; +import static org.labkey.studies.StudiesSchema.TABLE_EXPECTED_TIMEPOINTS; +import static org.labkey.studies.StudiesSchema.TABLE_STUDIES; +import static org.labkey.studies.StudiesSchema.TABLE_TIMEPOINT_TO_DATE; import static org.labkey.studies.query.LookupSetsManager.TABLE_LOOKUPS; import static org.labkey.studies.query.LookupSetsManager.TABLE_LOOKUP_SETS; public class StudiesUserSchema extends SimpleUserSchema { + private static final Logger _log = LogHelper.getLogger(StudiesUserSchema.class, "Messages related to Studies Service"); + private static final String TABLE_EVENT_TYPES = "studyEventTypes"; + public StudiesUserSchema(User user, Container container, DbSchema dbschema) { super(StudiesSchema.NAME, "", user, container, dbschema); @@ -39,6 +56,7 @@ public StudiesUserSchema(User user, Container container, DbSchema dbschema) public Set getTableNames() { Set available = new CaseInsensitiveTreeSet(super.getTableNames()); + available.add(TABLE_EVENT_TYPES); available.addAll(getPropertySetNames().keySet()); return Collections.unmodifiableSet(available); @@ -104,6 +122,30 @@ else if (TABLE_LOOKUPS.equalsIgnoreCase(name)) ret.addPermissionMapping(ReadPermission.class, StudiesDataAdminPermission.class); return ret.init(); } + else if (TABLE_STUDIES.equalsIgnoreCase(name)) + { + return createStudyDesignTable(name, cf, false); + } + else if (TABLE_COHORTS.equalsIgnoreCase(name)) + { + return createStudyDesignTable(name, cf, true); + } + else if (TABLE_ANCHOR_EVENTS.equalsIgnoreCase(name)) + { + return createStudyDesignTable(name, cf, true); + } + else if (TABLE_EXPECTED_TIMEPOINTS.equalsIgnoreCase(name)) + { + return createStudyDesignTable(name, cf, true); + } + else if (TABLE_TIMEPOINT_TO_DATE.equalsIgnoreCase(name)) + { + return createStudyDesignTable(name, cf, true); + } + else if (TABLE_EVENT_TYPES.equalsIgnoreCase(name)) + { + return createEventTypesTable(getContainer()); + } //try to find it in propertySets Map> nameMap = getPropertySetNames(); @@ -113,6 +155,21 @@ else if (TABLE_LOOKUPS.equalsIgnoreCase(name)) return super.createTable(name, cf); } + private TableInfo createStudyDesignTable(String name, ContainerFilter cf, boolean addTriggers) + { + CustomPermissionsTable ret = new CustomPermissionsTable<>(this, createSourceTable(name), cf); + ret.addPermissionMapping(InsertPermission.class, StudiesDataAdminPermission.class); + ret.addPermissionMapping(UpdatePermission.class, StudiesDataAdminPermission.class); + ret.addPermissionMapping(DeletePermission.class, StudiesDataAdminPermission.class); + + if (addTriggers) + { + ret.addTriggerFactory(new StudiesTriggerFactory()); + } + + return ret.init(); + } + private LookupSetTable createForPropertySet(StudiesUserSchema us, ContainerFilter cf, String setName, Map map) { SchemaTableInfo table = _dbSchema.getTable(TABLE_LOOKUPS); @@ -122,4 +179,47 @@ private LookupSetTable createForPropertySet(StudiesUserSchema us, ContainerFilte ret.addPermissionMapping(DeletePermission.class, StudiesDataAdminPermission.class); return ret.init(); } + + private TableInfo createEventTypesTable(Container container) + { + StringBuilder sql = new StringBuilder("SELECT * FROM ("); + final int startLength = sql.length(); + StudiesService.get().getEventProviders(container).forEach(ep -> { + if (sql.length() > startLength) + { + sql.append("UNION ALL\n"); + } + + sql.append("SELECT "). + append("'").append(ep.getName()).append("' AS name, "). + append("'").append(ep.getLabel()).append("' AS label, "). + append("'").append(ep.getDescription()).append("' AS description\n"); + }); + + sql.append(") x"); + + QueryDefinition qd = QueryService.get().createQueryDef(getUser(), getContainer(), this, TABLE_EVENT_TYPES); + qd.setSql(sql.toString()); + + List errors = new ArrayList<>(); + TableInfo ti = qd.getTable(errors, true); + if (!errors.isEmpty()) + { + _log.error("Problem with studyEventTypes query"); + for (QueryException e : errors) + { + _log.error(e.getMessage()); + } + } + + if (ti instanceof AbstractTableInfo ati) + { + ati.setTitle("Study Event Types"); + ati.getMutableColumn("name").setLabel("Name"); + ati.getMutableColumn("label").setLabel("Label"); + ati.getMutableColumn("description").setLabel("Description"); + } + + return ti; + } } diff --git a/Studies/src/org/labkey/studies/study/StudiesFilterProvider.java b/Studies/src/org/labkey/studies/study/StudiesFilterProvider.java new file mode 100644 index 000000000..956d36df4 --- /dev/null +++ b/Studies/src/org/labkey/studies/study/StudiesFilterProvider.java @@ -0,0 +1,73 @@ +package org.labkey.studies.study; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.TableInfo; +import org.labkey.api.laboratory.TabbedReportItem; +import org.labkey.api.laboratory.query.TabbedReportFilterProvider; +import org.labkey.api.module.Module; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.query.FieldKey; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.template.ClientDependency; +import org.labkey.studies.StudiesModule; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StudiesFilterProvider implements TabbedReportFilterProvider +{ + @Override + public Module getOwningModule() + { + return ModuleLoader.getInstance().getModule(StudiesModule.class); + } + + @Override + public Collection getClientDependencies() + { + return List.of(ClientDependency.fromPath("studies/panel/StudiesFilterType.js")); + } + + @Override + public String getXType() + { + return "studies-filtertype"; + } + + @Override + public String getLabel() + { + return "Study"; + } + + @Override + public String getInputValue() + { + return "studies"; + } + + @Override + public @NotNull Map getAdditionalFieldKeys(TableInfo ti, TabbedReportItem tri, Map overrides) + { + Map ret = new HashMap<>(); + + if (overrides.get("studyAssignmentFieldKey") == null) + { + FieldKey subject = tri.getSubjectIdFieldKey(); + if (subject != null) + { + subject = subject.getParent(); + } + + FieldKey fk = FieldKey.fromString(subject, "projects/allStudies"); + Map colMap = tri.getQueryCache().getColumns(ti, PageFlowUtil.set(fk)); + if (colMap.containsKey(fk)) + ret.put("studyAssignmentFieldKey", colMap.get(fk).getFieldKey()); + } + + return ret; + } +} diff --git a/Studies/src/org/labkey/studies/study/StudyEnrollmentEventProvider.java b/Studies/src/org/labkey/studies/study/StudyEnrollmentEventProvider.java new file mode 100644 index 000000000..9682541ef --- /dev/null +++ b/Studies/src/org/labkey/studies/study/StudyEnrollmentEventProvider.java @@ -0,0 +1,59 @@ +package org.labkey.studies.study; + +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.module.ModuleLoader; +import org.labkey.api.query.FieldKey; +import org.labkey.api.security.User; +import org.labkey.api.studies.study.AbstractEventProvider; +import org.labkey.api.study.DatasetTable; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.studies.StudiesModule; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class StudyEnrollmentEventProvider extends AbstractEventProvider +{ + public StudyEnrollmentEventProvider() + { + super("EnrollmentStart", "Enrollment Start", "This is the first date when the subject was assigned to the study, as defined in the study assignment table", ModuleLoader.getInstance().getModule(StudiesModule.class)); + } + + @Override + protected Map inferDatesRaw(Collection subjectList, Container c, User u) + { + TableInfo ti = getTable(c, u, "study", "assignment"); + if (ti == null) + { + return Collections.emptyMap(); + } + + if (ti instanceof DatasetTable ds) + { + Map ret = new HashMap<>(); + final String subjectCol = ds.getDataset().getStudy().getSubjectColumnName(); + new TableSelector(ti, PageFlowUtil.set(subjectCol, "date"), new SimpleFilter(FieldKey.fromString(subjectCol), subjectList, CompareType.IN), null).forEachResults(rs -> { + String subjectId = rs.getString(FieldKey.fromString(subjectCol)); + Date date = rs.getDate(FieldKey.fromString("date")); + + if (!ret.containsKey(subjectId) || date.before(ret.get(subjectId))) + { + ret.put(subjectId, date); + } + }); + + return ret; + } + else + { + throw new IllegalStateException("Expected study.assignment to be a DatasetTable"); + } + } +} diff --git a/cluster/api-src/org/labkey/api/cluster/ClusterResourceAllocator.java b/cluster/api-src/org/labkey/api/cluster/ClusterResourceAllocator.java index 8c014d064..ee4d41b0f 100644 --- a/cluster/api-src/org/labkey/api/cluster/ClusterResourceAllocator.java +++ b/cluster/api-src/org/labkey/api/cluster/ClusterResourceAllocator.java @@ -47,19 +47,19 @@ interface Factory /** * Additional lines to include in the condor submit script. These will be appended to the default script. */ - @Nullable void addExtraSubmitScriptLines(PipelineJob job, RemoteExecutionEngine engine, List existingExtraLines); + @Nullable void addExtraSubmitScriptLines(PipelineJob job, RemoteExecutionEngine engine, List existingExtraLines); /** * The memory, in GB, to use as -xmx for the LabKey java remote process */ @Nullable - default void processJavaOpts(PipelineJob job, RemoteExecutionEngine engine, @NotNull List existingJavaOpts) + default void processJavaOpts(PipelineJob job, RemoteExecutionEngine engine, @NotNull List existingJavaOpts) { } @NotNull - default Map getEnvironmentVars(PipelineJob job, RemoteExecutionEngine engine) + default Map getEnvironmentVars(PipelineJob job, RemoteExecutionEngine engine) { return Collections.emptyMap(); } diff --git a/discvrcore/api-src/org/labkey/api/discvrcore/annotation/UtilityAction.java b/discvrcore/api-src/org/labkey/api/discvrcore/annotation/UtilityAction.java new file mode 100644 index 000000000..2def94cf9 --- /dev/null +++ b/discvrcore/api-src/org/labkey/api/discvrcore/annotation/UtilityAction.java @@ -0,0 +1,16 @@ +package org.labkey.api.discvrcore.annotation; + +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) +public @interface UtilityAction +{ + String description(); + + String label(); +} \ No newline at end of file diff --git a/discvrcore/api-src/org/labkey/api/discvrcore/test/AbstractIntegrationTest.java b/discvrcore/api-src/org/labkey/api/discvrcore/test/AbstractIntegrationTest.java new file mode 100644 index 000000000..d952b222c --- /dev/null +++ b/discvrcore/api-src/org/labkey/api/discvrcore/test/AbstractIntegrationTest.java @@ -0,0 +1,36 @@ +package org.labkey.api.discvrcore.test; + +import org.junit.Assert; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.security.User; +import org.labkey.api.util.TestContext; + +public class AbstractIntegrationTest extends Assert +{ + protected static void doInitialSetUp(String projectName) + { + //pre-clean + doCleanup(projectName); + + Container project = ContainerManager.getForPath(projectName); + if (project == null) + { + ContainerManager.createContainer(ContainerManager.getRoot(), projectName, TestContext.get().getUser()); + } + } + + protected static void doCleanup(String projectName) + { + Container project = ContainerManager.getForPath(projectName); + if (project != null) + { + ContainerManager.delete(project, getUser()); + } + } + + protected static User getUser() + { + return TestContext.get().getUser(); + } +} diff --git a/discvrcore/src/org/labkey/discvrcore/DiscvrCoreController.java b/discvrcore/src/org/labkey/discvrcore/DiscvrCoreController.java index 1eb4526ef..4fc7a91d8 100644 --- a/discvrcore/src/org/labkey/discvrcore/DiscvrCoreController.java +++ b/discvrcore/src/org/labkey/discvrcore/DiscvrCoreController.java @@ -16,15 +16,279 @@ package org.labkey.discvrcore; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.labkey.api.action.ConfirmAction; +import org.labkey.api.action.SimpleErrorView; +import org.labkey.api.action.SimpleRedirectAction; +import org.labkey.api.action.SimpleViewAction; import org.labkey.api.action.SpringActionController; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.DbSequenceManager; +import org.labkey.api.data.SqlExecutor; +import org.labkey.api.data.TableInfo; +import org.labkey.api.discvrcore.annotation.UtilityAction; +import org.labkey.api.module.Module; +import org.labkey.api.module.ModuleLoader; +import org.labkey.api.pipeline.PipelineUrls; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.query.UserSchema; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.util.DOM; +import org.labkey.api.util.GUID; +import org.labkey.api.util.LinkBuilder; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.util.URLHelper; +import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.HtmlView; +import org.labkey.api.view.NavTree; +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.TreeMap; + +import static javax.swing.Spring.width; +import static org.labkey.api.util.DOM.Attribute.valign; +import static org.labkey.api.util.DOM.at; +import static org.labkey.api.util.DOM.cl; public class DiscvrCoreController extends SpringActionController { private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(DiscvrCoreController.class); public static final String NAME = "discvrcore"; + private static final Logger _log = LogHelper.getLogger(DiscvrCoreController.class, "Messages from DISCVR Core Controller"); + public DiscvrCoreController() { setActionResolver(_actionResolver); } + + @UtilityAction(label = "Truncate Query Audit Log", description = "Provides a mechanism to truncate the query and dataset audit tables for a container") + @RequiresPermission(AdminPermission.class) + public static class TruncateQueryAuditLogAction extends ConfirmAction + { + @Override + public ModelAndView getConfirmView(Object o, BindException errors) throws Exception + { + setTitle("Truncate Query/Dataset Audit Logs"); + + return HtmlView.of("This will truncate the query and dataset audit logs for this container. Do you want to continue?"); + } + + @Override + public boolean handlePost(Object o, BindException errors) throws Exception + { + UserSchema us = AuditLogService.getAuditLogSchema(getUser(), getContainer()); + for (String tableName : Arrays.asList("DatasetAuditEvent", "QueryUpdateAuditEvent")) + { + TableInfo ti = us.getTable(tableName); + ti.getUpdateService().truncateRows(getUser(), getContainer(), null, null); + } + + return true; + } + + @Override + public void validateCommand(Object o, Errors errors) + { + + } + + @NotNull + @Override + public URLHelper getSuccessURL(Object o) + { + return PageFlowUtil.urlProvider(PipelineUrls.class).urlBegin(getContainer()); + } + } + + @RequiresPermission(AdminPermission.class) + public static class ShowUtilityActionsAction extends SimpleViewAction + { + @Override + public ModelAndView getView(Object form, BindException errors) + { + Map items = new TreeMap<>(); + Collection modules = getContainer().isRoot() ? ModuleLoader.getInstance().getModules() : getContainer().getActiveModules(); + for (Module m : modules) + { + m.getControllerNameToClass().forEach((key, controllerCls) -> { + Arrays.stream(controllerCls.getDeclaredClasses()).filter(x -> x.isAnnotationPresent(UtilityAction.class)).forEach(x -> { + if (Controller.class.isAssignableFrom(x)) + { + UtilityAction annot = x.getAnnotation(UtilityAction.class); + + Class actionClass = (Class)x; + items.put(annot.label(), DOM.TR( + DOM.TD(at(valign,"top"), LinkBuilder.labkeyLink(annot.label(), new ActionURL(actionClass, getContainer())).build()), + DOM.TD(at(valign,"top"), annot.description()) + )); + } + }); + }); + } + + return new HtmlView(DOM.TABLE(cl("labkey-data-region-legacy","labkey-show-borders"), items.values())); + } + + @Override + public void addNavTrail(NavTree root) + { + root.addChild("Utility & Management Actions"); + } + } + + @UtilityAction(label = "Move Workbook", description = "This will move this workbook to the selected folder, renaming this workbook to match the series in that container. Note: there are many reasons this can be problematic, so please do this with great care") + @RequiresPermission(AdminPermission.class) + public static class MoveWorkbookAction extends ConfirmAction + { + private Container _movedWb = null; + + @Override + public void validateCommand(MoveWorkbookForm form, Errors errors) + { + + } + + @Override + public ModelAndView getConfirmView(MoveWorkbookForm form, BindException errors) throws Exception + { + if (!getContainer().isWorkbook()) + { + errors.reject(ERROR_MSG, "This is only supported for workbooks"); + return new SimpleErrorView(errors); + } + + String sb = "This will move this workbook to the selected folder, renaming this workbook to match the series in that container. Note: there are many reasons this can be problematic, so please do this with great care

" + + ""; + + return new HtmlView(sb); + } + + @Override + public boolean handlePost(MoveWorkbookForm form, BindException errors) throws Exception + { + Container toMove = getContainer(); + if (!toMove.isWorkbook()) + { + errors.reject(ERROR_MSG, "This is only supported for workbooks"); + return false; + } + + if (StringUtils.trimToNull(form.getTargetContainer()) == null) + { + errors.reject(ERROR_MSG, "Must provide target container"); + return false; + } + + Container target = ContainerManager.getForPath(StringUtils.trimToNull(form.getTargetContainer())); + if (target == null) + { + target = ContainerManager.getForId(StringUtils.trimToNull(form.getTargetContainer())); + } + + if (target == null) + { + errors.reject(ERROR_MSG, "Unknown container: " + form.getTargetContainer()); + return false; + } + + if (target.isWorkbook()) + { + errors.reject(ERROR_MSG, "Target cannot be a workbook: " + form.getTargetContainer()); + return false; + } + + if (ContainerManager.isSystemContainer(target)) + { + errors.reject(ERROR_MSG, "Cannot move to system containers: " + form.getTargetContainer()); + return false; + } + + if (target.equals(toMove.getParent())) + { + errors.reject(ERROR_MSG, "Cannot move the workbook to its current parent: " + form.getTargetContainer()); + return false; + } + + //NOTE: transaction causing problems for larger sites? + //try (DbScope.Transaction transaction = CoreSchema.getInstance().getSchema().getScope().ensureTransaction()) + //{ + //first rename workbook to make unique + String tempName = new GUID().toString(); + int sortOrder = (int) DbSequenceManager.get(target, ContainerManager.WORKBOOK_DBSEQUENCE_NAME).next(); + _log.info("renaming workbook to in preparation for move from: " + toMove.getPath() + " to: " + tempName); + ContainerManager.rename(toMove, getUser(), tempName); + toMove = ContainerManager.getForId(toMove.getId()); + + //then move parent + _log.info("moving workbook from: " + toMove.getPath() + " to: " + target.getPath()); + ContainerManager.move(toMove, target, getUser()); + toMove = ContainerManager.getForId(toMove.getId()); + + //finally move to correct name + _log.info("renaming workbook from: " + toMove.getPath() + " to: " + sortOrder); + ContainerManager.rename(toMove, getUser(), String.valueOf(sortOrder)); + toMove.setSortOrder(sortOrder); + new SqlExecutor(CoreSchema.getInstance().getSchema()).execute("UPDATE core.containers SET SortOrder = ? WHERE EntityId = ?", toMove.getSortOrder(), toMove.getId()); + toMove = ContainerManager.getForId(toMove.getId()); + + //transaction.commit(); + _log.info("workbook move finished"); + + _movedWb = toMove; + //} + + return true; + } + + @NotNull + @Override + public URLHelper getSuccessURL(MoveWorkbookForm moveWorkbookForm) + { + if (_movedWb == null) + return getContainer().getStartURL(getUser()); + else + return _movedWb.getStartURL(getUser()); + } + } + + public static class MoveWorkbookForm + { + private String _targetContainer; + + public String getTargetContainer() + { + return _targetContainer; + } + + public void setTargetContainer(String targetContainer) + { + _targetContainer = targetContainer; + } + } + + // This allows registration of this action without creating a dependency between laboratory and discvrcore + @UtilityAction(label = "Set Table Increment Value", description = "This allows you to reset the current value for an auto-incrementing table") + @RequiresPermission(AdminPermission.class) + public class SetTableIncrementValueAction extends SimpleRedirectAction + { + @Override + public URLHelper getRedirectURL(Object o) throws Exception + { + return DetailsURL.fromString("laboratory/setTableIncrementValue.view", getContainer()).getActionURL(); + } + } } diff --git a/discvrcore/src/org/labkey/discvrcore/DiscvrCoreModule.java b/discvrcore/src/org/labkey/discvrcore/DiscvrCoreModule.java index f7486e70a..a99627487 100644 --- a/discvrcore/src/org/labkey/discvrcore/DiscvrCoreModule.java +++ b/discvrcore/src/org/labkey/discvrcore/DiscvrCoreModule.java @@ -18,8 +18,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.ContainerManager; import org.labkey.api.module.DefaultModule; import org.labkey.api.module.ModuleContext; +import org.labkey.api.query.DetailsURL; +import org.labkey.api.settings.AdminConsole; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.WebPartFactory; @@ -66,7 +69,7 @@ protected void init() @Override public void doStartup(ModuleContext moduleContext) { - + AdminConsole.addLink(AdminConsole.SettingsLinkType.Management, "site utility actions", DetailsURL.fromString("discvrcore/showUtilityActions.view", ContainerManager.getRoot()).getActionURL()); } @Override diff --git a/jbrowse/package-lock.json b/jbrowse/package-lock.json index a0f488c77..a59c3c97c 100644 --- a/jbrowse/package-lock.json +++ b/jbrowse/package-lock.json @@ -3133,9 +3133,9 @@ } }, "node_modules/@labkey/components": { - "version": "6.45.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.45.0.tgz", - "integrity": "sha512-KT4C+NdlS6T54GF8jsde3cYm6Dt88AEZFw+dA39N83MGW3FiJ63s3DhF55dx8ImheCnchYlpK5xDF5/JI/Ux7A==", + "version": "6.50.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.50.1.tgz", + "integrity": "sha512-g6DDCg3rsoCzkJDTRAtUFdGskyufFipJB50mmgQhEgtyEEbaXB6rF/Ny8ytPCZOmpA5yL2+pEtqFoXlvyXBMDg==", "dependencies": { "@hello-pangea/dnd": "18.0.1", "@labkey/api": "1.41.2", @@ -5153,8 +5153,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dependencies": { "balanced-match": "^1.0.0" } @@ -7031,9 +7032,9 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", diff --git a/jbrowse/src/org/labkey/jbrowse/JBrowseFieldUtils.java b/jbrowse/src/org/labkey/jbrowse/JBrowseFieldUtils.java index 6e5e2df27..13d22ced1 100644 --- a/jbrowse/src/org/labkey/jbrowse/JBrowseFieldUtils.java +++ b/jbrowse/src/org/labkey/jbrowse/JBrowseFieldUtils.java @@ -16,6 +16,7 @@ import org.labkey.jbrowse.model.JsonFile; import java.io.File; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -80,7 +81,7 @@ public static Map getIndexedFields(JsonFile json fd.lock(); } - _cache.put(key, ret); + _cache.put(key, Collections.unmodifiableMap(ret)); } // Clone cached results: @@ -150,7 +151,7 @@ public static Map getGenotypeDependentFields(@Nu fd.lock(); } - _cache.put(key, ret); + _cache.put(key, Collections.unmodifiableMap(ret)); } // Clone cached results: diff --git a/jbrowse/test/src/org/labkey/test/tests/external/labModules/JBrowseTest.java b/jbrowse/test/src/org/labkey/test/tests/external/labModules/JBrowseTest.java index b267d0c76..a8fbe8888 100644 --- a/jbrowse/test/src/org/labkey/test/tests/external/labModules/JBrowseTest.java +++ b/jbrowse/test/src/org/labkey/test/tests/external/labModules/JBrowseTest.java @@ -574,7 +574,7 @@ private void testFullTextSearch() throws Exception // all // this should return 143 results. We can't make any other assumptions about the content String url = "/jbrowse/" + getProjectName() + "/luceneQuery.view?sessionId=" + sessionId + "&trackId=" + trackId + "&searchString=all&pageSize=143"; - beginAt(url); + beginAt(url, WAIT_FOR_PAGE * 2); waitForText("data"); waitAndClick(Locator.tagWithId("a", "rawdata-tab")); String jsonString = getText(Locator.tagWithClass("pre", "data")); diff --git a/singlecell/resources/chunks/FindClustersAndDimRedux.R b/singlecell/resources/chunks/FindClustersAndDimRedux.R index 1aadac4c8..b7fdc7c0f 100644 --- a/singlecell/resources/chunks/FindClustersAndDimRedux.R +++ b/singlecell/resources/chunks/FindClustersAndDimRedux.R @@ -19,7 +19,21 @@ for (datasetId in names(seuratObjects)) { printName(datasetId) seuratObj <- readSeuratRDS(seuratObjects[[datasetId]]) - seuratObj <- CellMembrane::FindClustersAndDimRedux(seuratObj, minDimsToUse = minDimsToUse, useLeiden = useLeiden) + if (all(is.null(clusterResolutions))) { + clusterResolutions <- c(0.2, 0.4, 0.6, 0.8, 1.2) + } else if (is.character(clusterResolutions)) { + clusterResolutionsOrig <- clusterResolutions + clusterResolutions <- gsub(clusterResolutions, pattern = ' ', replacement = '') + clusterResolutions <- unlist(strsplit(clusterResolutions, split = ',')) + clusterResolutions <- as.numeric(clusterResolutions) + if (any(is.na(clusterResolutions))) { + stop(paste0('Some values for clusterResolutions were not numeric: ', clusterResolutionsOrig)) + } + } else { + stop('Must provide a value for clusterResolutions') + } + + seuratObj <- CellMembrane::FindClustersAndDimRedux(seuratObj, minDimsToUse = minDimsToUse, useLeiden = useLeiden, clusterResolutions = clusterResolutions) saveData(seuratObj, datasetId) diff --git a/singlecell/src/org/labkey/singlecell/SingleCellProvider.java b/singlecell/src/org/labkey/singlecell/SingleCellProvider.java index fa9eaf6c5..c9bd534f6 100644 --- a/singlecell/src/org/labkey/singlecell/SingleCellProvider.java +++ b/singlecell/src/org/labkey/singlecell/SingleCellProvider.java @@ -164,29 +164,24 @@ public List getTabbedReportItems(Container c, User u) List items = new ArrayList<>(); - NavItem owner = getDataNavItems(c, u).get(0); String category = NAME; QueryCache cache = new QueryCache(); TabbedReportItem stims = new QueryTabbedReportItem(cache, this, SingleCellSchema.NAME, SingleCellSchema.TABLE_SAMPLES, "Single Cell Samples", category); - stims.setOwnerKey(owner.getPropertyManagerKey()); items.add(stims); TabbedReportItem sorts = new QueryTabbedReportItem(cache, this, SingleCellSchema.NAME, SingleCellSchema.TABLE_SORTS, "Single Cell Sorts", category); sorts.setSubjectIdFieldKey(FieldKey.fromString("sampleId/subjectId")); sorts.setSampleDateFieldKey(FieldKey.fromString("sampleId/date")); - sorts.setAllProjectsFieldKey(FieldKey.fromString("sampleId/allProjectsPivot")); - sorts.setOverlappingProjectsFieldKey(FieldKey.fromString("sampleId/overlappingProjectsPivot")); - sorts.setOwnerKey(owner.getPropertyManagerKey()); + sorts.setKeyOverride("allProjectsFieldName", FieldKey.fromString("sampleId/allProjectsPivot")); + sorts.setKeyOverride("overlappingProjectsFieldName", FieldKey.fromString("sampleId/overlappingProjectsPivot")); items.add(sorts); TabbedReportItem cdnas = new QueryTabbedReportItem(cache, this, SingleCellSchema.NAME, SingleCellSchema.TABLE_CDNAS, "Single Cell Libraries", category); - cdnas.setSubjectIdFieldKey(FieldKey.fromString("sortId/sampleId/subjectId")); cdnas.setSampleDateFieldKey(FieldKey.fromString("sortId/sampleId/date")); - cdnas.setAllProjectsFieldKey(FieldKey.fromString("sortId/sampleId/allProjectsPivot")); - cdnas.setOverlappingProjectsFieldKey(FieldKey.fromString("sortId/sampleId/overlappingProjectsPivot")); - cdnas.setOwnerKey(owner.getPropertyManagerKey()); + cdnas.setKeyOverride("allProjectsFieldName", FieldKey.fromString("sortId/sampleId/allProjectsPivot")); + cdnas.setKeyOverride("overlappingProjectsFieldName", FieldKey.fromString("sortId/sampleId/overlappingProjectsPivot")); items.add(cdnas); return items; diff --git a/singlecell/src/org/labkey/singlecell/pipeline/singlecell/FindClustersAndDimRedux.java b/singlecell/src/org/labkey/singlecell/pipeline/singlecell/FindClustersAndDimRedux.java index 86e16b494..bd0baf9ed 100644 --- a/singlecell/src/org/labkey/singlecell/pipeline/singlecell/FindClustersAndDimRedux.java +++ b/singlecell/src/org/labkey/singlecell/pipeline/singlecell/FindClustersAndDimRedux.java @@ -26,8 +26,9 @@ public Provider() }}, 15), SeuratToolParameter.create("useLeiden", "Use Leiden Clustering", "If true, FindClusters() will use algorith=4 (leiden), as opposed to the default (louvain)", "checkbox", new JSONObject(){{ - }}, false) - ), null, null); + }}, false), + SeuratToolParameter.create("clusterResolutions", "Cluster Resolutions", "A comma-separated list of resolution. If blank, it will default to 0.2, 0.4, 0.6, 0.8, 1.2", "textfield", null, null) + ), null, "0.2,0.4,0.6,0.8,1.2"); }