diff --git a/resources/queries/targetedms/qcannotation/.qview.xml b/resources/queries/targetedms/qcannotation/.qview.xml index 402f7a544..c593875bf 100644 --- a/resources/queries/targetedms/qcannotation/.qview.xml +++ b/resources/queries/targetedms/qcannotation/.qview.xml @@ -4,6 +4,8 @@ + + diff --git a/resources/queries/targetedms/qcannotationtype/.qview.xml b/resources/queries/targetedms/qcannotationtype/.qview.xml index 920ef55d0..c2bdf5bb7 100644 --- a/resources/queries/targetedms/qcannotationtype/.qview.xml +++ b/resources/queries/targetedms/qcannotationtype/.qview.xml @@ -4,6 +4,7 @@ + diff --git a/resources/schemas/dbscripts/postgresql/targetedms-26.002-26.003.sql b/resources/schemas/dbscripts/postgresql/targetedms-26.002-26.003.sql new file mode 100644 index 000000000..d82c6dba8 --- /dev/null +++ b/resources/schemas/dbscripts/postgresql/targetedms-26.002-26.003.sql @@ -0,0 +1,3 @@ +ALTER TABLE targetedms.QCAnnotationType ADD COLUMN isShareable BOOLEAN DEFAULT FALSE; +ALTER TABLE targetedms.QCAnnotation ADD COLUMN instrumentModel VARCHAR(255); +ALTER TABLE targetedms.QCAnnotation ADD COLUMN instrumentSerialNumber VARCHAR(255); \ No newline at end of file diff --git a/resources/schemas/targetedms.xml b/resources/schemas/targetedms.xml index 569b890e1..11f0d83d0 100644 --- a/resources/schemas/targetedms.xml +++ b/resources/schemas/targetedms.xml @@ -1262,6 +1262,7 @@ + @@ -1307,6 +1308,8 @@ Annotation Type The category of the event + + diff --git a/src/org/labkey/targetedms/TargetedMSManager.java b/src/org/labkey/targetedms/TargetedMSManager.java index c2c228a33..0a4ad113c 100644 --- a/src/org/labkey/targetedms/TargetedMSManager.java +++ b/src/org/labkey/targetedms/TargetedMSManager.java @@ -17,6 +17,8 @@ package org.labkey.targetedms; import com.google.common.base.Joiner; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; @@ -207,7 +209,8 @@ public static TargetedMSManager get() public static List getSampleFileChromInfos(SampleFile sampleFile) { - return new TableSelector(getTableInfoSampleFileChromInfo(), new SimpleFilter(FieldKey.fromParts("SampleFileId"), sampleFile.getId()), new Sort("TextId")).getArrayList(SampleFileChromInfo.class); } + return new TableSelector(getTableInfoSampleFileChromInfo(), new SimpleFilter(FieldKey.fromParts("SampleFileId"), sampleFile.getId()), new Sort("TextId")).getArrayList(SampleFileChromInfo.class); + } public static SampleFileChromInfo getSampleFileChromInfo(int id, Container c) { @@ -548,7 +551,8 @@ public static TableInfo getTableInfoQuantificationSettings() return getSchema().getTable(TargetedMSSchema.TABLE_QUANTIIFICATION_SETTINGS); } - public static TableInfo getTableInfoCalibrationCurve() { + public static TableInfo getTableInfoCalibrationCurve() + { return getSchema().getTable(TargetedMSSchema.TABLE_CALIBRATION_CURVE); } @@ -628,19 +632,23 @@ public static TableInfo getTableInfoSkylineAuditLogMessage() return getSchema().getTable(TargetedMSSchema.TABLE_SKYLINE_AUDITLOG_MESSAGE); } - public static TableInfo getTableInfoListDefinition() { + public static TableInfo getTableInfoListDefinition() + { return getSchema().getTable(TargetedMSSchema.TABLE_LIST_DEFINITION); } - public static TableInfo getTableInfoListColumnDefinition() { + public static TableInfo getTableInfoListColumnDefinition() + { return getSchema().getTable(TargetedMSSchema.TABLE_LIST_COLUMN_DEFINITION); } - public static TableInfo getTableInfoListItem() { + public static TableInfo getTableInfoListItem() + { return getSchema().getTable(TargetedMSSchema.TABLE_LIST_ITEM); } - public static TableInfo getTableInfoListItemValue() { + public static TableInfo getTableInfoListItemValue() + { return getSchema().getTable(TargetedMSSchema.TABLE_LIST_ITEM_VALUE); } @@ -1246,7 +1254,7 @@ public List getNickname(String name, TargetedMSSchema schema } List result = new ArrayList<>(dedupeAcrossContainers.values()); - + if (matches.isEmpty()) { String sql = "SELECT DISTINCT InstrumentNickname, " + @@ -3046,6 +3054,41 @@ private QueryUpdateService getNicknameUpdateService(User user, Container contain return Objects.requireNonNull(table.getUpdateService()); } + public static class InstrumentDetails + { + @Getter @Setter + private String instrumentSerialNumber; + @Getter @Setter + private String model; + @Getter @Setter + private Long instrumentId; + + public InstrumentDetails() + { + } + } + + public static List getInstrumentDetails(Container container) + { + SQLFragment sql = new SQLFragment("SELECT DISTINCT sf.InstrumentSerialNumber, i.Model, i.Id AS InstrumentId FROM "); + sql.append(getTableInfoSampleFile(), "sf"); + sql.append(" INNER JOIN "); + sql.append(getTableInfoInstrument(), "i"); + sql.append(" ON sf.InstrumentId = i.Id "); + sql.append(" INNER JOIN "); + sql.append(getTableInfoReplicate(), "rep"); + sql.append(" ON sf.ReplicateId = rep.Id "); + sql.append(" INNER JOIN "); + sql.append(getTableInfoRuns(), "r"); + sql.append(" ON rep.RunId = r.Id "); + sql.append(" WHERE r.Container = ?"); + sql.add(container); + + return new SqlSelector(getSchema(), sql).getArrayList(InstrumentDetails.class); + + } + + public void deleteNickname(InstrumentNickname name, User user) throws SQLException, BatchValidationException, QueryUpdateServiceException, InvalidKeyException { getNicknameUpdateService(user, name.getContainer()). @@ -3071,4 +3114,15 @@ public void saveNickname(InstrumentNickname name, User user) throws SQLException insertRows(user, name.getContainer(), Arrays.asList(row), errors, null, null); } } + + public static boolean isQCAnnotationTypeShareable(int qcAnnotationTypeId) + { + SQLFragment sql = new SQLFragment("SELECT IsShareable FROM "); + sql.append(getTableInfoQCAnnotationType()); + sql.append(" WHERE Id = ?"); + sql.add(qcAnnotationTypeId); + + Boolean isShareable = new SqlSelector(getSchema(), sql).getObject(Boolean.class); + return isShareable != null && isShareable; + } } diff --git a/src/org/labkey/targetedms/TargetedMSModule.java b/src/org/labkey/targetedms/TargetedMSModule.java index 5a14f90d9..921e46202 100644 --- a/src/org/labkey/targetedms/TargetedMSModule.java +++ b/src/org/labkey/targetedms/TargetedMSModule.java @@ -231,7 +231,7 @@ public String getName() @Override public Double getSchemaVersion() { - return 26.002; + return 26.003; } @Override diff --git a/src/org/labkey/targetedms/query/QCAnnotationTable.java b/src/org/labkey/targetedms/query/QCAnnotationTable.java index bac7b231e..5af2f2484 100644 --- a/src/org/labkey/targetedms/query/QCAnnotationTable.java +++ b/src/org/labkey/targetedms/query/QCAnnotationTable.java @@ -15,13 +15,27 @@ */ package org.labkey.targetedms.query; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.TableInfo; import org.labkey.api.gwt.client.AuditBehaviorType; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.DefaultQueryUpdateService; +import org.labkey.api.query.DuplicateKeyException; import org.labkey.api.query.QueryForeignKey; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; import org.labkey.api.query.SimpleUserSchema; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; import org.labkey.targetedms.TargetedMSManager; import org.labkey.targetedms.TargetedMSSchema; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + import static org.labkey.targetedms.query.GuideSetTable.appendFormatLabel; /** @@ -44,4 +58,64 @@ public QCAnnotationTable(TargetedMSSchema schema, ContainerFilter cf) appendFormatLabel(getMutableColumn("EndDate")); setAuditBehavior(AuditBehaviorType.DETAILED); } + + @Override + public QueryUpdateService getUpdateService() + { + TableInfo table = getRealTable(); + if (table != null) + { + return new DefaultQueryUpdateService(this, getRealTable()) + { + @Override + public List> insertRows(User user, Container container, List> rows, BatchValidationException errors, @Nullable Map configParameters, @Nullable Map extraScriptContext) throws SQLException, QueryUpdateServiceException, DuplicateKeyException + { + List> resultRows = new java.util.ArrayList<>(); + for (Map row : rows) + { + try + { + // Check if the QCAnnotationType is shareable + int qcAnnotationTypeId = (Integer) row.get("QCAnnotationTypeId"); + boolean isShareable = TargetedMSManager.isQCAnnotationTypeShareable(qcAnnotationTypeId); + + if (isShareable) + { + List instruments = TargetedMSManager.getInstrumentDetails(getContainer()); + if (instruments.isEmpty()) + { + resultRows.add(super.insertRow(user, container, row)); + } + else + { + for (TargetedMSManager.InstrumentDetails instrument : instruments) + { + Map newRow = new java.util.HashMap<>(row); + newRow.put("instrumentModel", instrument.getModel()); + newRow.put("instrumentSerialNumber", instrument.getInstrumentSerialNumber()); + newRow.put("Container", getContainer().getId()); + resultRows.add(super.insertRow(user, container, newRow)); + } + } + } + else + { + resultRows.add(super.insertRow(user, container, row)); + } + } + catch (ValidationException e) + { + errors.addRowError(e); + } + } + + if (errors.hasErrors()) + return null; + + return resultRows; + } + }; + } + return null; + } } diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index acbf03751..5aa66774a 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -1301,19 +1301,98 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { getAnnotationData: function() { this.setLoadingMsg(); - var config = this.getReportConfig(); + let config = this.getReportConfig(); - var annotationSql = "SELECT qca.Id AS qcAnnotationId, qca.Date, qca.Description, qca.Created, qca.CreatedBy.DisplayName, qcat.Id AS qcAnnotationTypeId, qcat.Name, qcat.Color FROM qcannotation qca JOIN qcannotationtype qcat ON qcat.Id = qca.QCAnnotationTypeId"; + let annotationSql = "SELECT qca.Id AS qcAnnotationId, qca.Date, qca.Description, qca.Created, qca.CreatedBy.DisplayName, qcat.Id AS qcAnnotationTypeId, qcat.Name, qcat.Color, qca.container.Path AS ContainerPath FROM qcannotation qca JOIN qcannotationtype qcat ON qcat.Id = qca.QCAnnotationTypeId"; // Filter on start/end dates - var separator = " WHERE "; + let dateFilter = ""; if (config.StartDate) { - annotationSql += separator + "CAST(Date AS Date) >= '" + config.StartDate + "'"; - separator = " AND "; + dateFilter += " AND CAST(Date AS Date) >= '" + config.StartDate + "'"; } if (config.EndDate) { - annotationSql += separator + "CAST(Date AS Date) <= '" + config.EndDate + "'"; + dateFilter += " AND CAST(Date AS Date) <= '" + config.EndDate + "'"; } + annotationSql += " WHERE 1=1 " + dateFilter; + + let handleAnnotationData = function(data) { + let annotationData = data ? data.rows : []; + + // Check if there is an instrument attached to the current container from samplefile table. + LABKEY.Query.executeSql({ + schemaName: 'targetedms', + sql: "SELECT DISTINCT InstrumentId.Model, InstrumentSerialNumber, InstrumentNickname FROM samplefile", + scope: this, + success: function(instrumentData) { + if (instrumentData && instrumentData.rows && instrumentData.rows.length > 0) { + let instrumentFilter = ""; + let separator = ""; + for (let i = 0; i < instrumentData.rows.length; i++) { + let row = instrumentData.rows[i]; + let model = row["Model"]; + let serial = row["InstrumentSerialNumber"]; + let nickname = row["InstrumentNickname"]; + + instrumentFilter += separator + "("; + let innerSep = ""; + if (model) { + instrumentFilter += "(qca.instrumentModel = '" + model + "'"; + innerSep = " AND "; + } else { + instrumentFilter += "(qca.instrumentModel IS NULL"; + innerSep = " AND "; + } + + if (serial) { + instrumentFilter += innerSep + "qca.instrumentSerialNumber = '" + serial + "')"; + } else { + instrumentFilter += innerSep + "qca.instrumentSerialNumber IS NULL)"; + } + + if (nickname) { + instrumentFilter += " OR qca.instrumentNickName = '" + nickname + "'"; + } + instrumentFilter += ")"; + separator = " OR "; + } + + let sharedAnnotationSql = "SELECT qca.Id AS qcAnnotationId, qca.Date, qca.Description, qca.Created, qca.CreatedBy.DisplayName, qcat.Id AS qcAnnotationTypeId, qcat.Name, qcat.Color, qca.container.Path AS ContainerPath " + + "FROM qcannotation qca " + + "JOIN qcannotationtype qcat ON qcat.Id = qca.QCAnnotationTypeId " + + "WHERE qcat.IsShareable = true AND (" + instrumentFilter + ")" + dateFilter; + + LABKEY.Query.executeSql({ + schemaName: 'targetedms', + sql: sharedAnnotationSql, + containerFilter: LABKEY.Query.containerFilter.allFolders, + scope: this, + success: function(sharedData) { + if (sharedData && sharedData.rows) { + // add shared annotations but avoid duplicates if they were already in the first list + let existingIds = {}; + for (let j = 0; j < annotationData.length; j++) { + existingIds[annotationData[j].qcAnnotationId] = true; + } + for (let k = 0; k < sharedData.rows.length; k++) { + if (!existingIds[sharedData.rows[k].qcAnnotationId]) { + annotationData.push(sharedData.rows[k]); + } + } + } + this.processAnnotationData({rows: annotationData}); + }, + failure: this.failureHandler + }); + } else { + this.processAnnotationData({rows: annotationData}); + } + }, + failure: function() { + // if instrument fetch fails, just proceed with what we have + this.processAnnotationData({rows: annotationData}); + } + }); + }; LABKEY.Query.executeSql({ schemaName: 'targetedms', @@ -1321,7 +1400,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { sort: 'Date', containerFilter: LABKEY.Query.containerFilter.currentPlusProjectAndShared, scope: this, - success: this.processAnnotationData, + success: handleAnnotationData, failure: this.failureHandler }); }, @@ -1909,23 +1988,37 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { .attr("d", this.annotationShape(4)).attr('transform', transformAcc) .style("fill", colorAcc).style("stroke", colorAcc); - // add hover text for the annotation details - annotations.append("title") - .text(function(d) { - return "Created By: " + d['DisplayName'] + ", " - + "\nType: " + d['Name'] + ", " - + "\nDate: " + me.formatDate(new Date(d['Date']), true) + ", " - + "\nDescription: " + d['Description']; - }); - - // add some mouseover effects for fun - var mouseOn = function(pt, strokeWidth) { + // add mouseover effects for fun + let mouseOn = function(pt, strokeWidth, d) { d3.select(pt).transition().duration(800).attr("stroke-width", strokeWidth).ease("elastic"); + + if (!pt._tippy) { + let content = "Created By: " + d['DisplayName'] + ", " + + "
Type: " + d['Name'] + ", " + + "
Date: " + me.formatDate(new Date(d['Date']), true) + ", " + + "
Description: " + d['Description']; + + if (d['ContainerPath'] && d['ContainerPath'] !== LABKEY.ActionURL.getContainer()) { + let containerPath = d['ContainerPath']; + if (containerPath.startsWith('/')) { + containerPath = containerPath.substring(1); + } + content += ",
Shared From: " + containerPath; + } + + tippy(pt, { + content: content, + allowHTML: true, + arrow: true, + theme: 'light-border', + placement: 'top' + }); + } }; var mouseOff = function(pt) { d3.select(pt).transition().duration(800).attr("stroke-width", 1).ease("elastic"); }; - annotations.on("mouseover", function(){ return mouseOn(this, 3); }); + annotations.on("mouseover", function(d){ return mouseOn(this, 3, d); }); annotations.on("mouseout", function(){ return mouseOff(this); }); if (this.canUserEdit()) { @@ -1962,10 +2055,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { .style("opacity", 0); // Add mouseover effects for add-annotations - nonAnnotationGroups.append("title") - .text("Add annotation"); - - nonAnnotationGroups.on("mouseover", function () { + nonAnnotationGroups.on("mouseover", function (d) { d3.select(this).select(".add-annotation-background") .transition().duration(300) .style("opacity", 0) @@ -1974,6 +2064,15 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { .transition().duration(300) .style("opacity", 1) .style("cursor", "pointer"); + + if (!this._tippy) { + tippy(this, { + content: "Add annotation", + arrow: true, + theme: 'light-border', + placement: 'top' + }); + } }); nonAnnotationGroups.on("mouseout", function () { d3.select(this).select(".add-annotation-background")