diff --git a/resources/queries/targetedms/InstrumentSummaryByFolder.sql b/resources/queries/targetedms/InstrumentSummaryByFolder.sql index c1517515f..183c11336 100644 --- a/resources/queries/targetedms/InstrumentSummaryByFolder.sql +++ b/resources/queries/targetedms/InstrumentSummaryByFolder.sql @@ -1,11 +1,11 @@ SELECT - COUNT(ReplicateId.RunId) AS SkylineDocumentCount, + COUNT(DISTINCT ReplicateId.RunId) AS SkylineDocumentCount, COUNT(DISTINCT ReplicateId) AS ReplicateCount, MIN(AcquiredTime) AS FirstAcquisition, MAX(AcquiredTime) AS LastAcquisition, ReplicateId.RunId.Container, - InstrumentSerialNumber + InstrumentNickname FROM targetedms.SampleFile GROUP BY ReplicateId.RunId.Container, - InstrumentSerialNumber \ No newline at end of file + InstrumentNickname \ No newline at end of file diff --git a/resources/queries/targetedms/QCInstrumentSummary.sql b/resources/queries/targetedms/QCInstrumentSummary.sql index 17a14bd05..6490bc7a6 100644 --- a/resources/queries/targetedms/QCInstrumentSummary.sql +++ b/resources/queries/targetedms/QCInstrumentSummary.sql @@ -1,4 +1,5 @@ SELECT + sf.InstrumentNickname AS Nickname, sf.InstrumentId.model AS InstrumentName, sf.InstrumentSerialNumber AS SerialNumber, MIN(sf.AcquiredTime) AS StartDate, @@ -9,6 +10,7 @@ SELECT FROM targetedms.SampleFile sf INNER JOIN replicate rep ON sf.replicateId = rep.Id GROUP BY + sf.InstrumentNickname, sf.InstrumentSerialNumber, sf.InstrumentId.model, rep.runId \ No newline at end of file diff --git a/resources/queries/targetedms/samplefile/.qview.xml b/resources/queries/targetedms/samplefile/.qview.xml index b59e095bc..95364d908 100644 --- a/resources/queries/targetedms/samplefile/.qview.xml +++ b/resources/queries/targetedms/samplefile/.qview.xml @@ -9,8 +9,7 @@ - - + diff --git a/resources/schemas/dbscripts/postgresql/targetedms-25.003-25.004.sql b/resources/schemas/dbscripts/postgresql/targetedms-25.003-25.004.sql new file mode 100644 index 000000000..7fdd571dd --- /dev/null +++ b/resources/schemas/dbscripts/postgresql/targetedms-25.003-25.004.sql @@ -0,0 +1,18 @@ +CREATE TABLE targetedms.InstrumentNickname +( + Id BIGSERIAL NOT NULL, + + Container entityid NOT NULL, + Created TIMESTAMP, + CreatedBy USERID, + Modified TIMESTAMP, + ModifiedBy USERID, + + SerialNumber VARCHAR(200), + Model VARCHAR(300), + Nickname VARCHAR(200), + + CONSTRAINT PK_InstrumentNickname PRIMARY KEY (Id) +); +CREATE INDEX IDX_InstrumentNickname_Container ON targetedms.InstrumentNickname(Container); + diff --git a/resources/schemas/dbscripts/sqlserver/targetedms-25.003-25.004.sql b/resources/schemas/dbscripts/sqlserver/targetedms-25.003-25.004.sql new file mode 100644 index 000000000..f3585ebe9 --- /dev/null +++ b/resources/schemas/dbscripts/sqlserver/targetedms-25.003-25.004.sql @@ -0,0 +1,18 @@ +CREATE TABLE targetedms.InstrumentNickname +( + Id BIGINT IDENTITY(1, 1) NOT NULL, + + Container entityid NOT NULL, + Created DATETIME, + CreatedBy USERID, + Modified DATETIME, + ModifiedBy USERID, + + SerialNumber NVARCHAR(200), + Model NVARCHAR(300), + Nickname NVARCHAR(200), + + CONSTRAINT PK_InstrumentNickname PRIMARY KEY (Id) +); +CREATE INDEX IDX_InstrumentNickname_Container ON targetedms.InstrumentNickname(Container); + diff --git a/resources/schemas/targetedms.xml b/resources/schemas/targetedms.xml index 06e1181be..91f86ce52 100644 --- a/resources/schemas/targetedms.xml +++ b/resources/schemas/targetedms.xml @@ -1920,4 +1920,17 @@ + + + + + + + + + + + + +
diff --git a/src/org/labkey/targetedms/TargetedMSController.java b/src/org/labkey/targetedms/TargetedMSController.java index b2e725d2e..f1e329af4 100644 --- a/src/org/labkey/targetedms/TargetedMSController.java +++ b/src/org/labkey/targetedms/TargetedMSController.java @@ -20,6 +20,8 @@ import com.keypoint.PngEncoder; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import lombok.Getter; +import lombok.Setter; import org.apache.batik.dom.GenericDOMImplementation; import org.apache.batik.svggen.SVGGeneratorContext; import org.apache.batik.svggen.SVGGraphics2D; @@ -111,11 +113,14 @@ import org.labkey.api.protein.ProteinService; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.DetailsURL; +import org.labkey.api.query.DuplicateKeyException; import org.labkey.api.query.FieldKey; import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.InvalidKeyException; import org.labkey.api.query.QueryParam; import org.labkey.api.query.QueryService; import org.labkey.api.query.QuerySettings; +import org.labkey.api.query.QueryUpdateServiceException; import org.labkey.api.query.QueryView; import org.labkey.api.query.UserSchema; import org.labkey.api.query.ValidationException; @@ -168,6 +173,7 @@ import org.labkey.api.view.PopupMenu; import org.labkey.api.view.Portal; import org.labkey.api.view.RedirectException; +import org.labkey.api.view.UnauthorizedException; import org.labkey.api.view.VBox; import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.api.view.ViewContext; @@ -188,6 +194,7 @@ import org.labkey.targetedms.model.GuideSet; import org.labkey.targetedms.model.GuideSetKey; import org.labkey.targetedms.model.GuideSetStats; +import org.labkey.targetedms.model.InstrumentNickname; import org.labkey.targetedms.model.PeptideOutliers; import org.labkey.targetedms.model.PrecursorChromInfoLitePlus; import org.labkey.targetedms.model.QCPlotFragment; @@ -1022,7 +1029,7 @@ public Object execute(QCSummaryForm form, BindException errors) List> containers = new ArrayList<>(); // include the QC Summary properties for the current container - containers.add(getContainerQCSummaryProperties(getContainer(), getContainer(), false)); + containers.add(getContainerQCSummaryProperties(getContainer(), getContainer(), false, getUser())); // include the QC Summary properties for the direct subfolders, of type QC, that the user has read permission if (form.isIncludeSubfolders()) @@ -1042,7 +1049,7 @@ public Object execute(QCSummaryForm form, BindException errors) folderType = TargetedMSManager.getFolderType(bestContainer); if (bestContainer.hasPermission(getUser(), ReadPermission.class) && folderType == TargetedMSService.FolderType.QC) { - containers.add(getContainerQCSummaryProperties(bestContainer, container, true)); + containers.add(getContainerQCSummaryProperties(bestContainer, container, true, getUser())); } } } @@ -1052,7 +1059,7 @@ public Object execute(QCSummaryForm form, BindException errors) } } - private Map getContainerQCSummaryProperties(Container container, Container instrumentContainer, boolean isSubfolder) + private Map getContainerQCSummaryProperties(Container container, Container instrumentContainer, boolean isSubfolder, User user) { Map properties = new HashMap<>(); SQLFragment sql; @@ -1072,11 +1079,11 @@ private Map getContainerQCSummaryProperties(Container container, properties.put("lastImportDate", valueMap.get("lastImportDate")); // # sample files, count of rows in targetedms.SampleFile - sql = new SQLFragment("SELECT COUNT(s.Id) FROM ").append(TargetedMSManager.getTableInfoSampleFile(), "s"); - sql.append(" JOIN ").append(TargetedMSManager.getTableInfoReplicate(), "re").append(" ON s.ReplicateId = re.Id"); - sql.append(" JOIN ").append(TargetedMSManager.getTableInfoRuns(), "r").append(" ON re.RunId = r.Id"); - sql.append(" WHERE r.Container = ?").add(container.getId()); - properties.put("fileCount", new SqlSelector(TargetedMSSchema.getSchema(), sql).getObject(Integer.class)); + TargetedMSSchema schema = new TargetedMSSchema(user, container); + TableInfo sampleFileTable = schema.getTableOrThrow(TargetedMSSchema.TABLE_SAMPLE_FILE); + List instruments = new TableSelector(sampleFileTable, Collections.singleton("InstrumentNickname")).getArrayList(String.class); + properties.put("fileCount", instruments.size()); + properties.put("distinctInstruments", instruments.stream().filter(Objects::nonNull).distinct().sorted().toList()); // # precursors tracked, count of distinct precursors. Include peptides and small molecules sql = new SQLFragment("SELECT DISTINCT COALESCE(p.ModifiedSequence, "); @@ -1100,7 +1107,6 @@ private Map getContainerQCSummaryProperties(Container container, autoQCPingMap.put("isRecent", lastModified.getTime() >= timeoutMinutesAgo); } properties.put("autoQCPing", autoQCPingMap); - TargetedMSSchema schema = new TargetedMSSchema(getUser(), container); properties.put("metricCount", TargetedMSManager.getEnabledQCMetricConfigurations(schema).size()); return properties; @@ -4512,7 +4518,21 @@ protected CalibrationCurvesView createQueryView( public static class InstrumentForm extends QueryViewAction.QueryExportForm { + private long _id; + private String _name; + private String _model; private String _serialNumber; + private String _targetContainerId; + + public String getModel() + { + return _model; + } + + public void setModel(String model) + { + _model = model; + } public String getSerialNumber() { @@ -4523,6 +4543,104 @@ public void setSerialNumber(String serialNumber) { _serialNumber = serialNumber; } + + public String getTargetContainerId() + { + return _targetContainerId; + } + + public void setTargetContainerId(String targetContainerId) + { + _targetContainerId = targetContainerId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public long getId() + { + return _id; + } + + public void setId(long id) + { + _id = id; + } + } + + @RequiresPermission(UpdatePermission.class) + public static class SaveInstrumentNameAction extends FormHandlerAction + { + @Override + public void validateCommand(InstrumentForm target, Errors errors) + { + } + + @Override + public boolean handlePost(InstrumentForm form, BindException errors) throws SQLException, BatchValidationException, QueryUpdateServiceException, InvalidKeyException, DuplicateKeyException + { + Container targetContainer = ContainerManager.getForId(form.getTargetContainerId()); + if (targetContainer == null || !targetContainer.hasPermission(getUser(), UpdatePermission.class)) + { + throw new UnauthorizedException(); + } + + try (var t = TargetedMSSchema.getSchema().getScope().ensureTransaction()) + { + InstrumentNickname name; + if (form.getId() > 0) + { + name = TargetedMSManager.get().getNickname(form.getId(), getUser()); + if (name == null) + { + throw new NotFoundException(); + } + if (!name.getContainer().hasPermission(getUser(), UpdatePermission.class)) + { + // Insert a different record instead + name.setId(0); + } + else if (!name.getContainer().equals(targetContainer)) + { + // Moving from one container to another. Handle this by deleting the old and inserting a new + TargetedMSManager.get().deleteNickname(name, getUser()); + name.setId(0); + } + } + else + { + name = new InstrumentNickname(); + } + if (StringUtils.isEmpty(form.getName()) && name.getId() > 0) + { + TargetedMSManager.get().deleteNickname(name, getUser()); + } + else + { + name.setNickname(form.getName()); + name.setModel(form.getModel()); + name.setSerialNumber(form.getSerialNumber()); + name.setContainer(targetContainer); + TargetedMSManager.get().saveNickname(name, getUser()); + } + t.commit(); + } + + return true; + } + + @Override + public URLHelper getSuccessURL(InstrumentForm form) + { + return StringUtils.isBlank(form.getName()) ? getContainer().getStartURL(getUser()) : new ActionURL(ShowInstrumentAction.class, getContainer()).addParameter("name", form.getName()); + } } @RequiresPermission(ReadPermission.class) @@ -4540,7 +4658,7 @@ public ShowInstrumentAction() @Override public void addNavTrail(NavTree root) { - root.addChild("Instrument " + (_form == null ? "" : _form.getSerialNumber())); + root.addChild("Instrument " + (_form == null ? "" : _form.getName())); } @Override @@ -4552,7 +4670,7 @@ protected QueryView createQueryView(InstrumentForm form, BindException errors, b Sort sort = new Sort(); sort.appendSortColumn(FieldKey.fromParts("AcquiredTime"), Sort.SortDirection.DESC, false); settings.setBaseSort(sort); - settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentSerialNumber"), form.getSerialNumber())); + settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentNickname"), form.getName())); settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); return schema.createView(getViewContext(), settings, errors); @@ -4560,7 +4678,7 @@ protected QueryView createQueryView(InstrumentForm form, BindException errors, b if (FOLDER_SUMMARY.equalsIgnoreCase(dataRegion)) { QuerySettings settings = new QuerySettings(getViewContext(), FOLDER_SUMMARY, "InstrumentSummaryByFolder"); - settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentSerialNumber"), form.getSerialNumber())); + settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentNickname"), form.getName())); settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); return schema.createView(getViewContext(), settings, errors); @@ -4569,23 +4687,44 @@ protected QueryView createQueryView(InstrumentForm form, BindException errors, b } @Override - public ModelAndView getView(InstrumentForm form, BindException errors) throws Exception + public ModelAndView getView(InstrumentForm form, BindException errors) { - if (form.getSerialNumber() == null) + if (form.getName() == null) { - throw new NotFoundException("No instrument serial number specified"); + throw new NotFoundException("No instrument specified"); } _form = form; + TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); + List names = TargetedMSManager.get().getNickname(form.getName(), schema); + + if (names.isEmpty()) + { + throw new NotFoundException("No matching instruments found"); + } + + VBox result = new VBox(); + + for (InstrumentNickname name : names) + { + var nameView = new JspView<>("/org/labkey/targetedms/view/nickname.jsp", name); + nameView.setTitle("Instrument Info"); + nameView.setFrame(WebPartView.FrameType.PORTAL); + result.addView(nameView); + } + QueryView folderSummaryView = createQueryView(form, errors, false, FOLDER_SUMMARY); - folderSummaryView.setTitle("Data in this Server"); + folderSummaryView.setTitle("Summary by Folder"); folderSummaryView.setFrame(WebPartView.FrameType.PORTAL); QueryView sampleFileView = createQueryView(form, errors, false, TargetedMSSchema.TABLE_SAMPLE_FILE); - sampleFileView.setTitle("Data from this Instrument"); + sampleFileView.setTitle("Samples from " + form.getName()); sampleFileView.setFrame(WebPartView.FrameType.PORTAL); - return new VBox(folderSummaryView, sampleFileView); + result.addView(folderSummaryView); + result.addView(sampleFileView); + + return result; } } @@ -5324,7 +5463,7 @@ public static Integer addProteinSummaryViews(VBox box, PeptideGroup group, Targe { int seqId = selectedProtein.getSequenceId(); List combinedPeptideCharacteristics = new ArrayList<>(PeptideManager.getCombinedPeptideCharacteristics(group.getId(), replicateId)); - List modifiedPeptideCharacteristics = new ArrayList<>(PeptideManager.getModifiedPeptideCharacteristics(group.getId(), replicateId));; + List modifiedPeptideCharacteristics = new ArrayList<>(PeptideManager.getModifiedPeptideCharacteristics(group.getId(), replicateId)); List replicates = ReplicateManager.getReplicatesForRun(run.getRunId()); List msReplicates = new ArrayList<>(); @@ -5608,7 +5747,7 @@ public class ShowProteinConflictUiAction extends SimpleViewAction conflictProteinList = ConflictResultsManager.getConflictedProteins(getContainer()); - if(conflictProteinList.size() == 0) + if(conflictProteinList.isEmpty()) { errors.reject(ERROR_MSG, "Library folder "+getContainer().getPath()+" does not contain any conflicting proteins."); return new SimpleErrorView(errors, true); @@ -5807,7 +5946,7 @@ public class ShowPrecursorConflictUiAction extends SimpleViewAction conflictPrecursorList = ConflictResultsManager.getConflictedPrecursors(getContainer()); - if(conflictPrecursorList.size() == 0) + if(conflictPrecursorList.isEmpty()) { errors.reject(ERROR_MSG, "Library folder "+getContainer().getPath()+" does not contain any conflicting data."); return new SimpleErrorView(errors, true); @@ -6342,19 +6481,11 @@ public void addNavTrail(NavTree root) // ------------------------------------------------------------------------ // Actions to export chromatogram libraries // ------------------------------------------------------------------------ + @Setter + @Getter public static class DownloadForm { int revision; - - public int getRevision() - { - return revision; - } - - public void setRevision(int revision) - { - this.revision = revision; - } } @RequiresPermission(ReadPermission.class) public static class DownloadChromLibraryAction extends SimpleViewAction @@ -6364,7 +6495,7 @@ public ModelAndView getView(DownloadForm form, BindException errors) throws Exce { // Check if the folder has any representative data List representativeRunIds = TargetedMSManager.getCurrentRepresentativeRunIds(getContainer()); - if(representativeRunIds.size() == 0) + if(representativeRunIds.isEmpty()) { //errors.reject(ERROR_MSG, "Folder "+getContainer().getPath()+" does not contain any representative data."); //return new SimpleErrorView(errors, true); @@ -7290,6 +7421,8 @@ public static long getNumRankedTransitions(Container container) { /* * BEGIN RENAME CODE BLOCK */ + @Setter + @Getter public static class RunForm extends ReturnUrlForm { public enum PARAMS @@ -7300,26 +7433,6 @@ public enum PARAMS int run = 0; String columns; - public void setRun(int run) - { - this.run = run; - } - - public int getRun() - { - return run; - } - - public String getColumns() - { - return columns; - } - - public void setColumns(String columns) - { - this.columns = columns; - } - @Override public ActionURL getReturnActionURL() { @@ -7355,19 +7468,11 @@ public static ActionURL getRenameRunURL(Container c, TargetedMSRun run, ActionUR return url; } + @Setter + @Getter public static class RenameForm extends RunForm { private String description; - - public String getDescription() - { - return description; - } - - public void setDescription(String description) - { - this.description = description; - } } @RequiresPermission(UpdatePermission.class) diff --git a/src/org/labkey/targetedms/TargetedMSListener.java b/src/org/labkey/targetedms/TargetedMSListener.java index a192a0654..1bca6867d 100644 --- a/src/org/labkey/targetedms/TargetedMSListener.java +++ b/src/org/labkey/targetedms/TargetedMSListener.java @@ -70,6 +70,7 @@ public void containerDeleted(Container c, User user) LibSpectrumReader.clearLibCache(c); new SqlExecutor(TargetedMSManager.getSchema()).execute("DELETE FROM " + TargetedMSManager.getTableInfoQCEmailNotifications() + " WHERE Container = ?", c); + new SqlExecutor(TargetedMSManager.getSchema()).execute("DELETE FROM " + TargetedMSManager.getTableInfoInstrumentNickname() + " WHERE Container = ?", c); } @Override diff --git a/src/org/labkey/targetedms/TargetedMSManager.java b/src/org/labkey/targetedms/TargetedMSManager.java index 7706d0fdb..5d9c2d546 100644 --- a/src/org/labkey/targetedms/TargetedMSManager.java +++ b/src/org/labkey/targetedms/TargetedMSManager.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.cache.Cache; import org.labkey.api.cache.CacheManager; +import org.labkey.api.collections.CaseInsensitiveHashMap; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; @@ -43,6 +44,7 @@ import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; import org.labkey.api.data.dialect.SqlDialect; +import org.labkey.api.data.dialect.StandardDialectStringHandler; import org.labkey.api.data.statistics.MathStat; import org.labkey.api.data.statistics.StatsService; import org.labkey.api.exp.AbstractFileXarSource; @@ -64,15 +66,21 @@ import org.labkey.api.pipeline.PipelineService; import org.labkey.api.pipeline.PipelineValidationException; import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DuplicateKeyException; import org.labkey.api.query.FieldKey; +import org.labkey.api.query.InvalidKeyException; import org.labkey.api.query.QueryDefinition; import org.labkey.api.query.QueryException; import org.labkey.api.query.QuerySchema; import org.labkey.api.query.QueryService; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; import org.labkey.api.query.SchemaKey; import org.labkey.api.query.UserSchema; import org.labkey.api.security.User; import org.labkey.api.security.permissions.DeletePermission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.security.permissions.UpdatePermission; import org.labkey.api.targetedms.ITargetedMSRun; import org.labkey.api.targetedms.RepresentativeDataState; import org.labkey.api.targetedms.RunRepresentativeDataState; @@ -91,6 +99,7 @@ import org.labkey.targetedms.model.GuideSetStats; import org.labkey.api.targetedms.model.QCMetricConfiguration; import org.labkey.api.targetedms.model.QCMetricStatus; +import org.labkey.targetedms.model.InstrumentNickname; import org.labkey.targetedms.model.QCTraceMetricValues; import org.labkey.targetedms.model.RawMetricDataSet; import org.labkey.targetedms.model.passport.IKeyword; @@ -113,6 +122,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.sql.SQLException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -136,6 +146,7 @@ import static org.labkey.api.targetedms.TargetedMSService.FolderType.Library; import static org.labkey.api.targetedms.TargetedMSService.FolderType.LibraryProtein; import static org.labkey.api.targetedms.TargetedMSService.MODULE_NAME; +import static org.labkey.targetedms.TargetedMSSchema.TABLE_INSTRUMENT_NICKNAME; public class TargetedMSManager { @@ -458,6 +469,11 @@ public static TableInfo getTableInfoFoldChange() return getSchema().getTable(TargetedMSSchema.TABLE_FOLD_CHANGE); } + public static TableInfo getTableInfoInstrumentNickname() + { + return getSchema().getTable(TABLE_INSTRUMENT_NICKNAME); + } + public static TableInfo getTableInfoiRTPeptide() { return getSchema().getTable(TargetedMSSchema.TABLE_IRT_PEPTIDE); @@ -1131,6 +1147,86 @@ public static void deleteIncludingExperimentWrapper(Container c, User user) } } + public InstrumentNickname getNickname(long id, User user) + { + InstrumentNickname name = new TableSelector(getTableInfoInstrumentNickname()).getObject(id, InstrumentNickname.class); + if (name == null || !name.getContainer().hasPermission(user, ReadPermission.class)) + { + throw new NotFoundException(); + } + return name; + } + + private record NicknameKey(String serialNumber, String model) {} + + /** @return the matches in order of closest to furthest match, injecting a virtual option if the list is empty */ + public List getNickname(String name, TargetedMSSchema schema) + { + TableInfo info = schema.getTableOrThrow(TABLE_INSTRUMENT_NICKNAME, new ContainerFilter.CurrentPlusProjectAndShared(schema.getContainer(), schema.getUser())); + List matches = new TableSelector(info, new SimpleFilter(FieldKey.fromParts("Nickname"), name), null).getArrayList(InstrumentNickname.class); + + Map dedupeAcrossContainers = new HashMap<>(); + // Closest is from the current container + addNameMatch(dedupeAcrossContainers, matches, schema.getContainer()); + // Next closest is from the project + @Nullable Container project = schema.getContainer().getProject(); + if (project != null && !project.equals(schema.getContainer())) + { + addNameMatch(dedupeAcrossContainers, matches, schema.getContainer().getProject()); + } + Container shared = ContainerManager.getSharedContainer(); + // Furthest is from /Shared + if (!schema.getContainer().equals(shared)) + { + addNameMatch(dedupeAcrossContainers, matches, shared); + } + + List result = new ArrayList<>(dedupeAcrossContainers.values()); + + if (matches.isEmpty()) + { + String sql = "SELECT DISTINCT InstrumentNickname, " + + "InstrumentId.Model AS Model, " + + "InstrumentSerialNumber AS SerialNumber " + + "FROM targetedms.SampleFile WHERE InstrumentNickname = " + + new StandardDialectStringHandler().quoteStringLiteral(name) + + " ORDER By InstrumentNickname, InstrumentId.Model, InstrumentSerialNumber"; + TableSelector selector = QueryService.get().selector(schema, sql); + result = selector.getArrayList(InstrumentNickname.class); + Container targetContainer; + if (shared.hasPermission(schema.getUser(), UpdatePermission.class)) + { + targetContainer = shared; + } + else if (project != null && project.hasPermission(schema.getUser(), UpdatePermission.class)) + { + targetContainer = project; + } + else + { + targetContainer = schema.getContainer(); + } + for (InstrumentNickname instrumentNickname : result) + { + instrumentNickname.setContainer(targetContainer); + } + } + + return result; + } + + private void addNameMatch(Map result, List matches, Container container) + { + for (InstrumentNickname match : matches) + { + if (match.getContainer().equals(container)) + { + NicknameKey key = new NicknameKey(match.getSerialNumber(), match.getModel()); + result.putIfAbsent(key, match); + } + } + } + /** * Delete just the targetedms run and its child tables * Pulled out into separate method so could be called by itself from data handlers @@ -2851,4 +2947,38 @@ public void clearCachedEnabledQCMetrics(Container container) { getSchema().getScope().addCommitTask(() -> _metricCache.remove(container), DbScope.CommitTaskOption.IMMEDIATE, DbScope.CommitTaskOption.POSTCOMMIT, DbScope.CommitTaskOption.POSTROLLBACK); } + + @NotNull + private QueryUpdateService getNicknameUpdateService(User user, Container container) + { + TargetedMSSchema schema = new TargetedMSSchema(user, container); + TableInfo table = schema.getTableOrThrow(TABLE_INSTRUMENT_NICKNAME); + return Objects.requireNonNull(table.getUpdateService()); + } + + public void deleteNickname(InstrumentNickname name, User user) throws SQLException, BatchValidationException, QueryUpdateServiceException, InvalidKeyException + { + getNicknameUpdateService(user, name.getContainer()). + deleteRows(user, name.getContainer(), Arrays.asList(new CaseInsensitiveHashMap<>(Map.of("id", name.getId()))), null, null); + } + + public void saveNickname(InstrumentNickname name, User user) throws SQLException, BatchValidationException, QueryUpdateServiceException, InvalidKeyException, DuplicateKeyException + { + Map row = new CaseInsensitiveHashMap<>(); + row.put("nickname", name.getNickname()); + row.put("serialNumber", name.getSerialNumber()); + row.put("model", name.getModel()); + BatchValidationException errors = new BatchValidationException(); + if (name.getId() > 0) + { + row.put("id", name.getId()); + getNicknameUpdateService(user, name.getContainer()). + updateRows(user, name.getContainer(), Arrays.asList(row), Arrays.asList(row), errors, null, null); + } + else + { + getNicknameUpdateService(user, name.getContainer()). + insertRows(user, name.getContainer(), Arrays.asList(row), errors, null, null); + } + } } diff --git a/src/org/labkey/targetedms/TargetedMSModule.java b/src/org/labkey/targetedms/TargetedMSModule.java index fb7ee15f0..8b2a2d289 100644 --- a/src/org/labkey/targetedms/TargetedMSModule.java +++ b/src/org/labkey/targetedms/TargetedMSModule.java @@ -234,7 +234,7 @@ public String getName() @Override public Double getSchemaVersion() { - return 25.003; + return 25.004; } @Override diff --git a/src/org/labkey/targetedms/TargetedMSSchema.java b/src/org/labkey/targetedms/TargetedMSSchema.java index 0d0b3b36e..8bfdbf634 100644 --- a/src/org/labkey/targetedms/TargetedMSSchema.java +++ b/src/org/labkey/targetedms/TargetedMSSchema.java @@ -46,6 +46,7 @@ import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.query.ExpRunTable; import org.labkey.api.exp.query.ExpSchema; +import org.labkey.api.gwt.client.AuditBehaviorType; import org.labkey.api.module.Module; import org.labkey.api.query.CustomView; import org.labkey.api.query.DefaultQueryUpdateService; @@ -159,6 +160,7 @@ public class TargetedMSSchema extends UserSchema public static final String TABLE_MOLECULE_GROUP = "MoleculeGroup"; public static final String TABLE_PEPTIDE_GROUP_ANNOTATION = "PeptideGroupAnnotation"; public static final String TABLE_INSTRUMENT = "Instrument"; + public static final String TABLE_INSTRUMENT_NICKNAME = "InstrumentNickname"; public static final String TABLE_ISOTOPE_ENRICHMENT = "IsotopeEnrichment"; public static final String TABLE_ISOLATION_SCHEME = "IsolationScheme"; public static final String TABLE_ISOLATION_WINDOW = "IsolationWindow"; @@ -1606,7 +1608,8 @@ public DisplayColumn createRenderer(ColumnInfo colInfo) TABLE_INSTRUMENT_SCHEDULE.equalsIgnoreCase(name) || TABLE_RATE_TYPE.equalsIgnoreCase(name) || TABLE_INSTRUMENT_RATE.equalsIgnoreCase(name) || - TABLE_INSTRUMENT_USAGE_PAYMENT.equalsIgnoreCase(name)) + TABLE_INSTRUMENT_USAGE_PAYMENT.equalsIgnoreCase(name) || + TABLE_INSTRUMENT_NICKNAME.equalsIgnoreCase(name)) { var result = new FilteredTable<>(getSchema().getTable(name), this, cf) { @@ -1616,13 +1619,17 @@ public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Class getAllTableNames(boolean caseInsensitive) hs.add(TABLE_REPLICATE); hs.add(TABLE_REPLICATE_ANNOTATION); hs.add(TABLE_INSTRUMENT); + hs.add(TABLE_INSTRUMENT_NICKNAME); hs.add(TABLE_ISOTOPE_ENRICHMENT); hs.add(TABLE_GENERAL_MOLECULE_CHROM_INFO); hs.add(TABLE_GENERAL_MOLECULE_ANNOTATION); diff --git a/src/org/labkey/targetedms/chart/ComparisonChartMaker.java b/src/org/labkey/targetedms/chart/ComparisonChartMaker.java index 8f7b004f9..c742d3d42 100644 --- a/src/org/labkey/targetedms/chart/ComparisonChartMaker.java +++ b/src/org/labkey/targetedms/chart/ComparisonChartMaker.java @@ -16,6 +16,7 @@ package org.labkey.targetedms.chart; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.LegendItem; @@ -145,13 +146,18 @@ private Pair getTitleAndType(PeptideGroup p { if (molecule == null) { + if (peptideGroup == null) + { + throw new NotFoundException("Could not resolve molecule or group"); + } + return Pair.of(peptideGroup.getLabel(), ComparisonDataset.ChartType.MOLECULE_COMPARISON); } return Pair.of(molecule.getCustomIonName(), ComparisonDataset.ChartType.REPLICATE_COMPARISON); } public JFreeChart makeRetentionTimesChart(long replicateId, PeptideGroup peptideGroup, - Molecule molecule, MoleculePrecursor precursor, + Molecule molecule, @Nullable MoleculePrecursor precursor, String groupByAnnotation, String filterByAnnotation, String value, boolean cvValues, User user, Container container) { @@ -332,7 +338,7 @@ else if (chartType == ComparisonDataset.ChartType.MOLECULE_COMPARISON) } private List getInputData(PeptideGroup peptideGroup, long replicateId, Peptide peptide, - Precursor precursor, ComparisonDataset.ChartType chartType, + @Nullable Precursor precursor, ComparisonDataset.ChartType chartType, User user, Container container) { List pciPlusList; @@ -353,7 +359,7 @@ private List getInputData(PeptideGroup peptideGroup, } private List getInputData(PeptideGroup peptideGroup, long replicateId, Molecule molecule, - MoleculePrecursor precursor, ComparisonDataset.ChartType chartType, + @Nullable MoleculePrecursor precursor, ComparisonDataset.ChartType chartType, User user, Container container) { List pciPlusList; diff --git a/src/org/labkey/targetedms/model/InstrumentNickname.java b/src/org/labkey/targetedms/model/InstrumentNickname.java new file mode 100644 index 000000000..0a11602db --- /dev/null +++ b/src/org/labkey/targetedms/model/InstrumentNickname.java @@ -0,0 +1,62 @@ +package org.labkey.targetedms.model; + +import org.labkey.api.data.Container; + +public class InstrumentNickname +{ + private long _id; + private String _nickname; + private String _model; + private String _serialNumber; + private Container _container; + + public long getId() + { + return _id; + } + + public void setId(long id) + { + _id = id; + } + + public String getNickname() + { + return _nickname; + } + + public void setNickname(String nickname) + { + _nickname = nickname; + } + + public String getModel() + { + return _model; + } + + public void setModel(String model) + { + _model = model; + } + + public String getSerialNumber() + { + return _serialNumber; + } + + public void setSerialNumber(String serialNumber) + { + _serialNumber = serialNumber; + } + + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } +} diff --git a/src/org/labkey/targetedms/query/SampleFileTable.java b/src/org/labkey/targetedms/query/SampleFileTable.java index 050177aac..f0d779491 100644 --- a/src/org/labkey/targetedms/query/SampleFileTable.java +++ b/src/org/labkey/targetedms/query/SampleFileTable.java @@ -120,6 +120,41 @@ public SampleFileTable(TargetedMSSchema schema, ContainerFilter cf, @Nullable Ta ExprColumn excludedColumn = new ExprColumn(this, "Excluded", excludedSQL, JdbcType.BOOLEAN); addColumn(excludedColumn); + SQLFragment nicknameSql = new SQLFragment("COALESCE("); + + // Find a nickname if we have one + SQLFragment nicknameToLimitSql = new SQLFragment("SELECT Nickname FROM (SELECT Nickname, CASE WHEN Container = ? THEN 3 WHEN Container = ? THEN 2 ELSE 1 END AS ContainerSort FROM \n"); + nicknameToLimitSql.append("(SELECT i.* FROM "); + nicknameSql.add(ContainerManager.getSharedContainer()); + nicknameSql.add(schema.getContainer().getProject()); + nicknameToLimitSql.append(TargetedMSManager.getTableInfoInstrument(), "i"); + nicknameToLimitSql.append(" WHERE i.Id = "); + nicknameToLimitSql.append(ExprColumn.STR_TABLE_ALIAS).append(".InstrumentId) i LEFT OUTER JOIN "); + nicknameToLimitSql.append(schema.getTableOrThrow(TargetedMSSchema.TABLE_INSTRUMENT_NICKNAME, ContainerFilter.Type.CurrentPlusProjectAndShared.create(schema)), "f"); + nicknameToLimitSql.append(" ON (f.SerialNumber = "); + nicknameToLimitSql.append(ExprColumn.STR_TABLE_ALIAS).append(".InstrumentSerialNumber"); + nicknameToLimitSql.append(" OR (f.SerialNumber IS NULL AND "); + nicknameToLimitSql.append(ExprColumn.STR_TABLE_ALIAS).append(".InstrumentSerialNumber"); + nicknameToLimitSql.append(" IS NULL)) AND (f.Model = i.Model OR (f.Model IS NULL AND i.Model IS NULL))) x ORDER BY ContainerSort\n"); + + getSqlDialect().limitRows(nicknameToLimitSql, 1); + + nicknameSql.append("("); + nicknameSql.append(nicknameToLimitSql); + nicknameSql.append("),\n"); + + // Alternatively, use the default name for the instrument (model - serial number) + nicknameSql.append("(SELECT COALESCE("); + nicknameSql.append(getSqlDialect().concatenate("x.Model", "' - '", ExprColumn.STR_TABLE_ALIAS + ".InstrumentSerialNumber")); + nicknameSql.append(", ").append(ExprColumn.STR_TABLE_ALIAS).append(".InstrumentSerialNumber, x.Model)"); + nicknameSql.append(" FROM (SELECT Model FROM "); + nicknameSql.append(TargetedMSManager.getTableInfoInstrument(), "i"); + nicknameSql.append(" WHERE i.Id = "); + nicknameSql.append(ExprColumn.STR_TABLE_ALIAS).append(".InstrumentId) x)"); + nicknameSql.append(") "); + ExprColumn nicknameCol = new ExprColumn(this, "InstrumentNickname", nicknameSql, JdbcType.VARCHAR, getColumn("InstrumentSerialNumber"), getColumn("InstrumentId")); + addColumn(nicknameCol); + // Special handling for a sample identifier annotation. Inject it even if this folder doesn't have it configured AnnotatedTargetedMSTable.AnnotationSettingForTyping idSetting = new AnnotatedTargetedMSTable.AnnotationSettingForTyping("SampleIdentifier", @@ -224,8 +259,8 @@ public SampleFileTable(TargetedMSSchema schema, ContainerFilter cf, @Nullable Ta downloadCol.setTextAlign("left"); downloadCol.setDisplayColumnFactory(DownloadLinkColumn::new); - DetailsURL instrumentURL = new DetailsURL(new ActionURL(TargetedMSController.ShowInstrumentAction.class, getContainer()), Collections.singletonMap("serialNumber", "InstrumentSerialNumber")); - getMutableColumn("InstrumentSerialNumber").setURL(instrumentURL); + DetailsURL instrumentURL = new DetailsURL(new ActionURL(TargetedMSController.ShowInstrumentAction.class, getContainer()), Collections.singletonMap("name", "InstrumentNickname")); + getMutableColumnOrThrow("InstrumentNickname").setURL(instrumentURL); } @Override @@ -250,7 +285,7 @@ public List getDefaultVisibleColumns() FieldKey.fromParts("File"), FieldKey.fromParts("Download"), FieldKey.fromParts("AcquiredTime"), - FieldKey.fromParts("InstrumentSerialNumber") + FieldKey.fromParts("InstrumentNickname") )); // Find the columns that have values for the run of interest, and include them in the set of columns in the default @@ -262,7 +297,6 @@ public List getDefaultVisibleColumns() aggregates.add(new Aggregate(FieldKey.fromParts("ReplicateId", "SampleType"), Aggregate.BaseType.MAX)); aggregates.add(new Aggregate(FieldKey.fromParts("ReplicateId", "AnalyteConcentration"), Aggregate.BaseType.MAX)); aggregates.add(new Aggregate(FieldKey.fromParts("ReplicateId", "SampleDilutionFactor"), Aggregate.BaseType.MAX)); - aggregates.add(new Aggregate(FieldKey.fromParts("InstrumentId"), Aggregate.BaseType.MAX)); // Also search for values for any replicate annotations being used in this container for (AnnotatedTargetedMSTable.AnnotationSettingForTyping annotation : getUserSchema().getAnnotationSettings("replicate", ContainerFilter.current(getUserSchema()))) diff --git a/src/org/labkey/targetedms/view/nickname.jsp b/src/org/labkey/targetedms/view/nickname.jsp new file mode 100644 index 000000000..1db5e1bec --- /dev/null +++ b/src/org/labkey/targetedms/view/nickname.jsp @@ -0,0 +1,91 @@ +<% +/* + * Copyright (c) 2013-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.targetedms.TargetedMSController" %> +<%@ page import="org.labkey.targetedms.model.InstrumentNickname" %> +<%@ page import="org.labkey.api.security.permissions.UpdatePermission" %> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.data.ContainerManager" %> +<%@ page import="java.util.Map" %> +<%@ page import="java.util.LinkedHashMap" %> +<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<% + JspView currentView = HttpView.currentView(); + InstrumentNickname name = currentView.getModelBean(); + Map targetContainers = new LinkedHashMap<>(); + Container shared = ContainerManager.getSharedContainer(); + if (shared.hasPermission(getUser(), UpdatePermission.class)) + { + targetContainers.put(shared.getId(), "Server-wide"); + } + Container project = getContainer().getProject(); + if (project != null && project.hasPermission(getUser(), UpdatePermission.class)) + { + targetContainers.putIfAbsent(project.getId(), "In this project, " + project.getName()); + } + if (getContainer().hasPermission(getUser(), UpdatePermission.class)) + { + targetContainers.putIfAbsent(getContainer().getId(), "In this folder, " + getContainer().getPath()); + } + %> + +<%-- Always render the form, but conditionally render its elements--%> + + <% if (!targetContainers.isEmpty()) { %> + + + + <% } %> + + + + + + + + + + + + + + + <% if (!targetContainers.containsKey(name.getContainer().getId())) { %> + + + + + <% } %> + <% if (!targetContainers.isEmpty()) { %> + + + + + <% } %> +
Model<%= h(name.getModel()) %>
Serial Number<%= h(name.getSerialNumber()) %>
<% if (targetContainers.isEmpty()) { %><%= h(name.getNickname()) %><% } else { %><% } %>
Currently saved in + <%= h(name.getContainer().getPath()) %> +
+ + <%= button("Save").submit(true) %> +
+
+ diff --git a/src/org/labkey/targetedms/view/renameRun.jsp b/src/org/labkey/targetedms/view/renameRun.jsp index 44ecefd6f..446cfbe2e 100644 --- a/src/org/labkey/targetedms/view/renameRun.jsp +++ b/src/org/labkey/targetedms/view/renameRun.jsp @@ -22,7 +22,8 @@ <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <% - RenameBean bean = ((JspView) HttpView.currentView()).getModelBean(); + JspView currentView = HttpView.currentView(); + RenameBean bean = currentView.getModelBean(); %> <%=generateReturnUrlFormField(bean.returnUrl)%> diff --git a/test/src/org/labkey/test/tests/panoramapremium/TargetedMSIsotopologueTest.java b/test/src/org/labkey/test/tests/panoramapremium/TargetedMSIsotopologueTest.java index fa61956e4..78f6faea7 100644 --- a/test/src/org/labkey/test/tests/panoramapremium/TargetedMSIsotopologueTest.java +++ b/test/src/org/labkey/test/tests/panoramapremium/TargetedMSIsotopologueTest.java @@ -16,12 +16,10 @@ @BaseWebDriverTest.ClassTimeout(minutes = 3) public class TargetedMSIsotopologueTest extends TargetedMSPremiumTest { - protected static final String ISOTOPOLOGUE_FILE_ANNOTATED = "PRM_7x5mix_A40010_QEHF_examples_v3.sky.zip"; - @BeforeClass public static void initProject() { - TargetedMSIsotopologueTest init = (TargetedMSIsotopologueTest) getCurrentTest(); + TargetedMSIsotopologueTest init = getCurrentTest(); init.doInit(); } diff --git a/test/src/org/labkey/test/tests/panoramapremium/TargetedMSQCPremiumTest.java b/test/src/org/labkey/test/tests/panoramapremium/TargetedMSQCPremiumTest.java index 2d780c903..714c0ed1c 100644 --- a/test/src/org/labkey/test/tests/panoramapremium/TargetedMSQCPremiumTest.java +++ b/test/src/org/labkey/test/tests/panoramapremium/TargetedMSQCPremiumTest.java @@ -202,10 +202,9 @@ public void testTraceMetric() timeValueOptions.put("Last", "Last"); timeValueOptions.put("Min", "Min"); timeValueOptions.put("Max", "Max"); - String skyFile = "SampleFileChromInfo.sky.zip"; setUpFolder(projectName, FolderType.QC); - importData(skyFile); + importData(SAMPLE_FILE_CHROM_INFO); addNewTimeTraceMetrics(metrics.get("First"), timeValueOptions.get("First"), traceName); addNewTimeTraceMetrics(metrics.get("Min"), timeValueOptions.get("Min"), "ColumnPressure (channel 4)"); @@ -225,13 +224,13 @@ public void testTraceMetric() log("Delete run and verify trace metric values are deleted"); clickTab("Runs"); TargetedMSRunsTable runsTable = new TargetedMSRunsTable(this); - runsTable.deleteRun(skyFile); + runsTable.deleteRun(SAMPLE_FILE_CHROM_INFO); goToSchemaBrowser(); traceValuesTable = viewQueryData("targetedms", "QCTraceMetricValues"); assertEquals("Values in QCTraceMetricValues are not deleted on deleting run", 0, traceValuesTable.getDataRowCount()); log("Reimport run and verify QCTraceMetricValues has values after import"); - importData(skyFile, 2); + importData(SAMPLE_FILE_CHROM_INFO, 2); goToSchemaBrowser(); traceValuesTable = viewQueryData("targetedms", "QCTraceMetricValues"); assertTrue("Trace values after import are not present", traceValuesTable.getDataRowCount() > 0); diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSExperimentalQCLinkTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSExperimentalQCLinkTest.java index cd47f6526..df4163c9a 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSExperimentalQCLinkTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSExperimentalQCLinkTest.java @@ -21,7 +21,6 @@ @BaseWebDriverTest.ClassTimeout(minutes = 4) public class TargetedMSExperimentalQCLinkTest extends TargetedMSTest { - private static final String SKY_FILE_EXPERIMENT = "SProCoPTutorial.zip"; private static final String SKY_FILE_QC = "SProCoPTutorial-QCFolderData.zip"; private static final String QC_FOLDER_1 = "Test Project QC Folder 1"; private static final String QC_FOLDER_2 = "Test Project QC Folder 2"; @@ -30,14 +29,14 @@ public class TargetedMSExperimentalQCLinkTest extends TargetedMSTest @BeforeClass public static void initProject() { - TargetedMSExperimentalQCLinkTest init = (TargetedMSExperimentalQCLinkTest) getCurrentTest(); + TargetedMSExperimentalQCLinkTest init = getCurrentTest(); init.doInit(); } private void doInit() { setupFolder(FolderType.Experiment); - importData(SKY_FILE_EXPERIMENT); + importData(SProCoP_FILE); log("Creating one test QC folder with same data"); setUpFolder(QC_FOLDER_1, FolderType.QC); @@ -98,7 +97,7 @@ public void testInstrumentSummaryPage() @Test public void testLinkExperimentalQC() { - String expRange = "Skyline File: " + SKY_FILE_EXPERIMENT + ", " + + String expRange = "Skyline File: " + SProCoP_FILE + ", " + "Start: 2013-08-09 11:39:00, " + "End: 2013-08-27 14:45:49, " + "Mean: 14.669, Std Dev: 0.501, " + @@ -139,8 +138,8 @@ public void testLinkExperimentalQC() log("Verify experiment toolbar is present"); checker().verifyTrue("Experiment date range toolbar is not present", - isElementPresent(Locator.linkContainingText(SKY_FILE_EXPERIMENT))); - clickAndWait(Locator.linkContainingText(SKY_FILE_EXPERIMENT)); + isElementPresent(Locator.linkContainingText(SProCoP_FILE))); + clickAndWait(Locator.linkContainingText(SProCoP_FILE)); checker().verifyEquals("Did not navigate to experimental folder", getProjectName(), getCurrentContainer()); goBack(); diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentNicknameTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentNicknameTest.java new file mode 100644 index 000000000..7ee0f68e4 --- /dev/null +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentNicknameTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2016-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.tests.targetedms; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.query.DeleteRowsCommand; +import org.labkey.remoteapi.query.Filter; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.test.Locator; +import org.labkey.test.TestTimeoutException; +import org.labkey.test.util.ApiPermissionsHelper; +import org.labkey.test.util.PermissionsHelper; + +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +@Category({}) +public class TargetedMSInstrumentNicknameTest extends TargetedMSTest +{ + private static final String QC_SUB_FOLDER = "QC Subfolder"; + private static final String QC_SUB_SUB_FOLDER = "QC SubSubfolder"; + private static final String NON_QC_SUB_FOLDER = "NonQC Subfolder 3"; + public static final String Q_EXACTIVE = "Q Exactive"; + public static final String Q_EXACTIVE_SERIAL_ONLY = "Exactive Series slot #2384"; + public static final String Q_EXACTIVE_WITH_SERIAL = Q_EXACTIVE + " - " + Q_EXACTIVE_SERIAL_ONLY; + public static final String QTRAP = "4000 QTRAP - U02630409"; + public static final String AUTOMATED_TEST_NICKNAME_PREFIX = "AutomatedTestNickname"; + public static final String NICKNAME_1 = AUTOMATED_TEST_NICKNAME_PREFIX + "1" + TRICKY_CHARACTERS; + public static final String NICKNAME_2 = AUTOMATED_TEST_NICKNAME_PREFIX + "2" + TRICKY_CHARACTERS; + public static final String NICKNAME_3 = AUTOMATED_TEST_NICKNAME_PREFIX + "3" + TRICKY_CHARACTERS; + public static final String REPLICATE_NAME_WITHOUT_SERIAL = "QEHF_7x5_TRAP_PRM_30minG_ES800_200fmolOC_25Oct19_R2"; + public static final String FILE_PATH_WITHOUT_SERIAL = "C:\\Xcalibur\\data\\Bhavin\\2018\\October\\QEHF_QC_24Oct2018\\QEHF_7x5_TRAP_PRM_30minG_ES800_TRAP_200fmolOC_25Oct2018_R2.raw"; + public static final String REPLICATE_NAME_WITH_SERIAL = "QEHF_7x5_TRAP_PRM_30minG_ES800_200fmolOC_21Apr18_R2"; + public static final String FILE_PATH_WITH_SERIAL = "Z:\\QEHF_RawData\\Bhavin\\2018\\April\\SystemSuitability_7x5_Validation_16Apr2018\\QEHF_SysSuitStd_35AQUA_7x5_TRAP_PRM_30minG_ES800_400fmolOC_3axAdj17A_21Apr18_R2.raw"; + + @Override + protected String getProjectName() + { + return getClass().getSimpleName() + " Project"; + } + + @BeforeClass + public static void initProject() + { + TargetedMSInstrumentNicknameTest init = getCurrentTest(); + init.setupProjectWithSubfolders(); + init.importInitialData(); + } + + @Override + protected void doCleanup(boolean afterTest) throws TestTimeoutException + { + super.doCleanup(afterTest); + + // Clean out the nicknames set in /Shared + var command = new SelectRowsCommand("targetedms", "InstrumentNickname"); + command.setFilters(List.of(new Filter("Nickname", AUTOMATED_TEST_NICKNAME_PREFIX, Filter.Operator.STARTS_WITH))); + try + { + Connection connection = createDefaultConnection(); + var response = command.execute(connection, "/Shared"); + if (!response.getRows().isEmpty()) + { + var deleteCommand = new DeleteRowsCommand("targetedms", "InstrumentNickname"); + deleteCommand.setRows(response.getRows()); + deleteCommand.execute(connection, "/Shared"); + } + } + catch (IOException | CommandException e) + { + throw new RuntimeException(e); + } + } + + private void setupProjectWithSubfolders() + { + setupFolder(FolderType.QC); + + setupSubfolder(getProjectName(), QC_SUB_FOLDER, FolderType.QC); + setupSubfolder(getProjectName(), NON_QC_SUB_FOLDER, FolderType.Experiment); + + clickFolder(QC_SUB_FOLDER); + setupSubfolder(getProjectName(), QC_SUB_FOLDER, QC_SUB_SUB_FOLDER, FolderType.QC); + + _userHelper.createUser(USER); + + // give user reader permissions to all but FOLDER_1 + ApiPermissionsHelper permissionsHelper = new ApiPermissionsHelper(this); + permissionsHelper.addMemberToRole(USER, "Reader", PermissionsHelper.MemberType.user, getProjectName()); + permissionsHelper.addMemberToRole(USER, "Editor", PermissionsHelper.MemberType.user, getProjectName() + "/" + NON_QC_SUB_FOLDER); + } + + private void importInitialData() + { + goToProjectHome(); + importData(ISOTOPOLOGUE_FILE_ANNOTATED, 1, false, false); + + // Import the same file into a subfolders to test scoping + clickFolder(QC_SUB_FOLDER); + importData(ISOTOPOLOGUE_FILE_ANNOTATED, 1, false, false); + + clickFolder(QC_SUB_SUB_FOLDER); + importData(ISOTOPOLOGUE_FILE_ANNOTATED, 1, false, false); + + clickFolder(NON_QC_SUB_FOLDER); + importData(ISOTOPOLOGUE_FILE_ANNOTATED, 1, false, false); + // Only do DB maintenance on the last import in the sequence + importData(SAMPLE_FILE_CHROM_INFO, 2, false, true); + } + + @Test + public void testSubfolders() + { + goToProjectHome(); + Locator qExactiveLinkLocator = Locator.linkWithText(Q_EXACTIVE); + Locator qExactiveWithSerialLinkLocator = Locator.linkWithText(Q_EXACTIVE_WITH_SERIAL); + Locator nickname1LinkLocator = Locator.linkWithText(NICKNAME_1); + Locator nickname2LinkLocator = Locator.linkWithText(NICKNAME_2); + + // Default display should show both variants of the model/serial number + waitForElement(qExactiveLinkLocator); + assertElementPresent(qExactiveWithSerialLinkLocator); + + // Give a nickname that collapses them, saving in the default scope (server-side) + clickAndWait(qExactiveLinkLocator); + setFormElement(Locator.input("name"), NICKNAME_1); + clickButton("Save"); + goToProjectHome(); + waitAndClickAndWait(qExactiveWithSerialLinkLocator); + setFormElement(Locator.input("name"), NICKNAME_1); + clickButton("Save"); + + // Be sure that the new nickname is shown and the model/serial number aren't + waitForElement(nickname1LinkLocator); + assertElementNotPresent(qExactiveLinkLocator); + assertElementNotPresent(qExactiveWithSerialLinkLocator); + + clickFolder(QC_SUB_FOLDER); + waitForElement(nickname1LinkLocator); + assertElementNotPresent(qExactiveLinkLocator); + assertElementNotPresent(qExactiveWithSerialLinkLocator); + + clickAndWait(nickname1LinkLocator); + // We should see both model/serial numbers on the same page + assertTextPresent(Q_EXACTIVE, 4); // Twice on the page itself, twice in hidden form elements + assertTextPresent(Q_EXACTIVE_SERIAL_ONLY, 2); // Once on the page itself, once in hidden form elements + assertTextPresent( + getProjectName() + "/" + QC_SUB_FOLDER + "/" + QC_SUB_SUB_FOLDER, + getProjectName() + "/" + NON_QC_SUB_FOLDER, + + // Check for a sample without a serial number + REPLICATE_NAME_WITHOUT_SERIAL, FILE_PATH_WITHOUT_SERIAL, + + // And one with a serial number + REPLICATE_NAME_WITH_SERIAL, FILE_PATH_WITH_SERIAL + ); + + // Rename the nickname for the one with the serial number to make sure they split + setFormElement(Locator.input("name"), NICKNAME_2); + clickButton("Save"); + + assertTextPresent(Q_EXACTIVE_SERIAL_ONLY, 2); // Once visible on the page, once in a hidden form element + assertTextPresent(REPLICATE_NAME_WITH_SERIAL, FILE_PATH_WITH_SERIAL); + + String postImpersonationUrl = getDriver().getCurrentUrl(); + impersonateRole("Reader"); + assertTextPresent(Q_EXACTIVE_SERIAL_ONLY, 1); // Just the visible element, no form and hidden inputs for readers + stopImpersonating(); + beginAt(postImpersonationUrl); + + // Now resave, scoped to the folder + Locator.XPathLocator targetContainerLocator = Locator.name("targetContainerId"); + selectOptionByTextContaining(targetContainerLocator.findElement(getDriver()), "In this folder"); + clickButton("Save"); + + goToDashboard(); + // We should see the current folder showing both nicknames + waitForElement(nickname1LinkLocator); + waitForElement(nickname2LinkLocator); + // We should also see the subfolder showing the nickname for one and the serial number for the other + waitForElement(qExactiveWithSerialLinkLocator); + + // The subfolder should only see one of the nicknames + clickFolder(QC_SUB_SUB_FOLDER); + waitForElement(nickname1LinkLocator); + assertElementPresent(qExactiveWithSerialLinkLocator); + assertElementNotPresent(nickname2LinkLocator); + + // Now try a non-QC folder, which should be inheriting the first nickname but not the second + clickFolder(NON_QC_SUB_FOLDER); + clickAndWait(Locator.linkWithText(ISOTOPOLOGUE_FILE_ANNOTATED)); + clickAndWait(Locator.linkWithText("6 replicates")); + assertElementPresent(nickname1LinkLocator); + assertElementPresent(qExactiveWithSerialLinkLocator); + // We should also get links to the QC folders with data from the same instrument + assertElementPresent(Locator.linkWithText(getProjectName())); + assertElementPresent(Locator.linkWithText(QC_SUB_FOLDER)); + assertElementPresent(Locator.linkWithText(QC_SUB_SUB_FOLDER)); + } + + @Test + public void testNonSiteAdmin() + { + goToProjectHome(); + clickFolder(NON_QC_SUB_FOLDER); + clickAndWait(Locator.linkWithText(SAMPLE_FILE_CHROM_INFO)); + clickAndWait(Locator.linkWithText("2 replicates")); + clickAndWait(Locator.linkWithText(QTRAP)); + + String postImpersonationUrl = getDriver().getCurrentUrl(); + // Check we don't let readers save + impersonateRole("Reader"); + assertTextPresent("Currently saved in"); + assertElementNotPresent(Locator.lkButton("Save")); + stopImpersonating(); + beginAt(postImpersonationUrl); + + Locator.XPathLocator targetContainerLocator = Locator.name("targetContainerId"); + // Impersonate a user who can only edit in subfolder + impersonate(USER); + // Ensure they can't save in /Shared or project + assertEquals("Wrong number of places to save the nickname", 1, targetContainerLocator.findElement(getDriver()).findElements(Locator.tag("option")).size()); + selectOptionByTextContaining(targetContainerLocator.findElement(getDriver()), "In this folder"); + setFormElement(Locator.input("name"), NICKNAME_3); + clickButton("Save"); + + clickAndWait(Locator.linkWithText(SAMPLE_FILE_CHROM_INFO)); + clickAndWait(Locator.linkWithText("2 replicates")); + assertElementPresent(Locator.linkWithText(NICKNAME_3)); + stopImpersonating(); + } +} diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCSummaryTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCSummaryTest.java index 954b32348..35ff83f64 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCSummaryTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCSummaryTest.java @@ -76,7 +76,7 @@ protected String getProjectName() @BeforeClass public static void initProject() { - TargetedMSQCSummaryTest init = (TargetedMSQCSummaryTest)getCurrentTest(); + TargetedMSQCSummaryTest init = getCurrentTest(); init.setupProjectWithSubfolders(); init.importInitialData(); } @@ -96,13 +96,14 @@ private void setupProjectWithSubfolders() private void importInitialData() { goToProjectHome(); - importData(SProCoP_FILE); + importData(SProCoP_FILE, 1, false, false); clickFolder(FOLDER_2); - importData(QC_1_FILE); + importData(QC_1_FILE, 1, false, false); clickFolder(FOLDER_2A); - importData(QC_2_FILE); + // Do DB maintenance after the last of the initial imports + importData(QC_2_FILE, 1, false, true); } private void setAutoQCPingTimeOut(String timeOutLength) @@ -321,7 +322,7 @@ private void validateAutoQCStatus(int webPartIndex, List iconClassValues for(String classValue : iconClassValues) { log("Validate that the autoQC icon has a value of '" + classValue + "' in its class property."); - assertTrue("AutoQC icon not as expected. Class did not contain '" + classValue + "'. Class: '" + tmpString + "'", tmpString.toLowerCase().contains(classValue)); + assertTrue("AutoQC icon not as expected. Class did not contain '" + classValue + "'. Class: '" + tmpString + "'", tmpString != null && tmpString.toLowerCase().contains(classValue)); } log("Validate bubble text is '" + bubbleText + "'"); @@ -363,7 +364,7 @@ private void validateSampleFile(int fileDetailIndex, Map fileDet if (!waitFor(() -> textSearcher.getMissingTexts(perBubbleTexts).isEmpty(), 10000)) { String actualText = textSearcher.getLastSearchedText(); - fail("The bubble text for the file detail not as expected. Bubble text: '" + actualText + "' Missing: '" + String.join(",", perBubbleTexts.stream().filter(s -> !actualText.contains(s)).collect(Collectors.toList())) + "'"); + fail("The bubble text for the file detail not as expected. Bubble text: '" + actualText + "' Missing: '" + perBubbleTexts.stream().filter(s -> !actualText.contains(s)).collect(Collectors.joining(",")) + "'"); } } qcSummaryWebPart.closeBubble(); @@ -423,9 +424,9 @@ private String doAutoQCPing(@Nullable String subFolder, AutoQCPing aqcp) } } - public class AutoQCPing extends PostCommand + public static class AutoQCPing extends PostCommand { - private String _softwareVersion; + private final String _softwareVersion; public AutoQCPing() { diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSTest.java index 85b02045c..131de3076 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSTest.java @@ -58,6 +58,8 @@ public abstract class TargetedMSTest extends BaseWebDriverTest protected static final String QC_3_FILE = "QC_3.sky.zip"; protected static final String QC_4_FILE = "QC_4.sky.zip"; protected static final String SKY_FILE_SMALLMOL_PEP = "smallmol_plus_peptides.sky.zip"; + protected static final String ISOTOPOLOGUE_FILE_ANNOTATED = "PRM_7x5mix_A40010_QEHF_examples_v3.sky.zip"; + protected static final String SAMPLE_FILE_CHROM_INFO = "SampleFileChromInfo.sky.zip"; protected static final String USER = "qcuser@targetedms.test"; private static ConfiguresSite siteConfigurer; @@ -218,6 +220,12 @@ protected void importData(@LoggedParam String file, int jobCount) @LogMethod protected void importData(@LoggedParam String file, int jobCount, boolean expectError) + { + importData(file, jobCount, expectError, true); + } + + @LogMethod + protected void importData(@LoggedParam String file, int jobCount, boolean expectError, boolean doDbMaintenance) { Locator.XPathLocator importButtonLoc = Locator.lkButton("Process and Import Data"); WebElement importButton = importButtonLoc.findElementOrNull(getDriver()); @@ -234,7 +242,10 @@ protected void importData(@LoggedParam String file, int jobCount, boolean expect waitForText("Skyline document import"); waitForPipelineJobsToComplete(jobCount, file, expectError); - runDbMaintenance(); + if (doDbMaintenance) + { + runDbMaintenance(); + } } private void runDbMaintenance() diff --git a/webapp/TargetedMS/js/QCSummaryPanel.js b/webapp/TargetedMS/js/QCSummaryPanel.js index d67ce2270..b319d57a6 100644 --- a/webapp/TargetedMS/js/QCSummaryPanel.js +++ b/webapp/TargetedMS/js/QCSummaryPanel.js @@ -21,13 +21,32 @@ Ext4.define('LABKEY.targetedms.QCSummary', { text: 'Loading...' }) - LABKEY.targetedms.QCMetricConfigLoader.getMetrics(this.initPanel, this); + this.qcPlotPanel.queryQCInstruments(this.getQCSummary, this); this.numSampleFileStats = config ? config.sampleLimit : 3; }, - initPanel : function(metrics) { - this.metricPropArr = metrics; - this.qcPlotPanel.queryQCInstruments(this.getQCSummary, this); + formatInstruments: function(container) { + if (container.distinctInstruments) { + if (container.distinctInstruments.length > 1) { + container.instrument = ' for multiple instruments:
    '; + for (let index = 0; index < container.distinctInstruments.length; index++) { + container.instrument += '
  • ' + this.formatInstrument(container.distinctInstruments[index], container.path) + '
  • '; + } + container.instrument += '
We recommend that each instrument use its own QC folder.'; + } + else if (container.distinctInstruments.length === 1 && container.distinctInstruments[0]) { + container.instrument = ' for ' + this.formatInstrument(container.distinctInstruments[0], container.path); + } + } + }, + + formatInstrument: function(name, containerPath) { + let result = Ext4.util.Format.htmlEncode(name ? name : 'unknown instrument'); + if (name) + result = '' + result + '' + return result; }, getQCSummary: function () { @@ -57,25 +76,14 @@ Ext4.define('LABKEY.targetedms.QCSummary', { container.showName = hasChildren; container.isParent = true; container.parentOnly = containers.length === 1; - if (this.qcPlotPanel.qcIntrumentsArr) { - if (this.qcPlotPanel.qcIntrumentsArr.length > 1) { - container.instrument = ' for multiple instruments:
    '; - for (let index = 0; index < this.qcPlotPanel.qcIntrumentsArr.length; index++) { - let currentInstrument = this.qcPlotPanel.qcIntrumentsArr[index]; - container.instrument += '
  • ' + Ext4.util.Format.htmlEncode(currentInstrument ? currentInstrument : 'unknown instrument') + '
  • '; - } - container.instrument += '
We recommend that each instrument use its own QC folder.'; - } - else if (this.qcPlotPanel.qcIntrumentsArr.length === 1 && this.qcPlotPanel.qcIntrumentsArr[0]) { - container.instrument = ' for ' + Ext4.util.Format.htmlEncode(this.qcPlotPanel.qcIntrumentsArr[0]); - } - } + this.formatInstruments(container); this.add(this.getContainerSummaryView(container, hasChildren, width)); // Add the set of child containers in an hbox layout if (hasChildren) { for (var i = 1; i < containers.length; i++) { container = containers[i]; + this.formatInstruments(container); container.showName = true; container.parentOnly = false; container.isParent = false;