diff --git a/resources/views/scheduleAllInstruments.view.xml b/resources/views/scheduleAllInstruments.view.xml
new file mode 100644
index 000000000..66bbcc043
--- /dev/null
+++ b/resources/views/scheduleAllInstruments.view.xml
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/resources/views/scheduleInstrument.html b/resources/views/scheduleInstrument.html
index 9372a6411..1526f1333 100644
--- a/resources/views/scheduleInstrument.html
+++ b/resources/views/scheduleInstrument.html
@@ -1,15 +1,26 @@
@@ -33,26 +44,26 @@
Add Instrument Time
@@ -161,9 +170,12 @@
Time Scheduled
let project = LABKEY.ActionURL.getParameter('project');
let instrument = LABKEY.ActionURL.getParameter('instrument');
+ // Parse as integers, defaulting to null if empty/invalid
+ project = project ? parseInt(project) || null : null;
+ instrument = instrument ? parseInt(instrument) || null : null;
if (!project) {
- alert('No project selected');
+ document.getElementById('calendar').innerText = 'Invalid or no project specified';
return;
}
@@ -174,21 +186,24 @@
Time Scheduled
LABKEY.Query.selectRows({
schemaName: 'targetedms',
queryName: 'msProject',
- sort: 'title',
- columns: 'Id,title',
+ sort: 'Title',
+ columns: 'Id,Title',
scope: this,
success: function (result) {
let rows = result.rows;
- let projectDropDown = '
Select Project: ';
- projectDropDown += '
';
+ let projectDropDown = document.getElementById('projectDropDown');
+ while (projectDropDown.firstChild) {
+ projectDropDown.removeChild(projectDropDown.firstChild);
+ }
for (let i = 0; i < rows.length; i++) {
- // if project == rows[i].Id, then select this option
- projectDropDown += '' + LABKEY.Utils.encodeHtml(rows[i].title) + ' ';
+ let option = document.createElement('option');
+ option.value = rows[i].Id;
+ option.text = rows[i].Title;
+ if (project === rows[i].Id) {
+ option.selected = true;
+ }
+ projectDropDown.appendChild(option);
}
- projectDropDown += ' ';
-
- jQuery('#projectDropDown').html(projectDropDown);
-
}
});
@@ -203,14 +218,21 @@
Time Scheduled
scope: this,
success: function (result) {
let rows = result.rows;
- let instrumentDropDown = '
Select Instrument: ';
- instrumentDropDown += '
';
+ let instrumentDropDown = document.getElementById('instrumentDropDown');
+ while (instrumentDropDown.firstChild) {
+ instrumentDropDown.removeChild(instrumentDropDown.firstChild);
+ }
+
for (let i = 0; i < rows.length; i++) {
- // if instrument == rows[i].Id, then select this option
- instrumentDropDown += '' + LABKEY.Utils.encodeHtml(rows[i].name) + ' ';
+ let option = document.createElement('option');
+ option.value = rows[i].Id;
+ option.text = rows[i].name;
+ if (instrument === rows[i].Id) {
+ option.selected = true;
+ }
+ instrumentDropDown.appendChild(option);
}
- instrumentDropDown += ' ';
- jQuery('#instrumentDropDown').html(instrumentDropDown);
+
loadInstrumentSchedule(function(instrumentSchedule) {
var calendarEl = document.getElementById('calendar');
@@ -228,11 +250,15 @@
Time Scheduled
eventContent: function(e) {
let content = '';
- let bgColor = (e.event.extendedProps.project == project ? e.backgroundColor : 'gray');
- let textColor = getContrastTextColor(stringToColor(bgColor));
- let dateStr = DateFormat.format.date(e.event.start, LABKEY.container.formats.timeFormat) + ' - ' + DateFormat.format.date(e.event.end, LABKEY.container.formats.timeFormat);
- let style = 'background-color: ' + bgColor + '; color: ' + textColor + ';' + 'width: 100%';
- content += '
'
+ let bgColor = e.event.extendedProps.project === project ? e.backgroundColor : 'gray';
+ const cl = e.event.extendedProps.project === project ? 'activeProjectEvent' : 'otherProjectEvent';
+ let textColor = ScheduleUtils.getContrastTextColor(ScheduleUtils.stringToColor(bgColor));
+ let timeFormatString = LABKEY.container.formats.timeFormat;
+ // Strip seconds and milliseconds
+ timeFormatString = timeFormatString.replace(':ss', '').replace('.SSS', '');
+ let dateStr = DateFormat.format.date(e.event.start, timeFormatString) + ' - ' + DateFormat.format.date(e.event.end, timeFormatString);
+ let style = 'background-color: ' + LABKEY.Utils.encodeHtml(bgColor) + '; color: ' + LABKEY.Utils.encodeHtml(textColor) + ';' + 'width: 100%';
+ content += '
'
+ '
' + LABKEY.Utils.encodeHtml(dateStr) + '
'
+ '
' + 'Project :  ' + LABKEY.Utils.encodeHtml(e.event.extendedProps.project) + '
'
+ '
';
@@ -240,7 +266,7 @@
Time Scheduled
},
eventMouseEnter: function (mouseEnterInfo) {
removeEventLog();
- let instrumentID = instrument ? instrument : $('#instrumentDropDown select[name="instrumentDropDown"]').val();
+ let instrumentID = instrument ? instrument : $('#instrumentDropDown').val();
fetchInstrumentCosts(instrumentID, mouseEnterInfo.event.start, mouseEnterInfo.event.end, false);
let content = '';
let dateStr = DateFormat.format.date(mouseEnterInfo.event.start, LABKEY.container.formats.dateTimeFormat) + ' - ' + DateFormat.format.date(mouseEnterInfo.event.end, LABKEY.container.formats.dateTimeFormat);
@@ -248,10 +274,6 @@
Time Scheduled
+ '
' + 'Project Id :  ' + LABKEY.Utils.encodeHtml(mouseEnterInfo.event.extendedProps.project) + '
'
+ '
' + 'Title :  ' + LABKEY.Utils.encodeHtml(mouseEnterInfo.event.title) + '
'
+ '
' + LABKEY.Utils.encodeHtml(dateStr) + '
'
-
- + '
[Delete] '
- + '
[Edit] '
- + '
[Edit Project] '
+ '
';
@@ -324,9 +346,7 @@
Time Scheduled
success: function (result) {
let rows = result.rows;
paymentMethodsData = rows;
- let paymentMethodDropDown = '
Select Payment Method(s):
'+
- 'Payment Method:  ';
- paymentMethodDropDown += '';
+ let paymentMethodDropDown = '';
for (let i = 0; i < rows.length; i++) {
paymentMethodDropDown += '' + LABKEY.Utils.encodeHtml(rows[i].name) + ' ';
}
@@ -344,57 +364,40 @@ Time Scheduled
schemaName: 'targetedms',
queryName: 'projectResearcher',
sort: 'name',
- columns: 'Id,researcher/displayName, project',
+ columns: 'Id,researcher/displayName,researcher/UserId,project',
filterArray: [
- LABKEY.Filter.create('project', project, LABKEY.Filter.Types.EQUAL)
+ LABKEY.Filter.create('project', project, LABKEY.Filter.Types.EQUAL),
+ LABKEY.Filter.create('researcher/UserId', project, LABKEY.Filter.Types.NONBLANK)
],
scope: this,
success: function (result) {
let rows = result.rows;
- let instrumentOperatorDropDown = 'Select Instrument Operator: ';
- instrumentOperatorDropDown += '';
+ let instrumentOperatorDropDown = document.getElementById('instrumentOperatorDropDown');
+ while (instrumentOperatorDropDown.firstChild) {
+ instrumentOperatorDropDown.removeChild(instrumentOperatorDropDown.firstChild);
+ }
for (let i = 0; i < rows.length; i++) {
- instrumentOperatorDropDown += '' + LABKEY.Utils.encodeHtml(rows[i]['researcher/displayName']) + ' ';
+ let option = document.createElement('option');
+ option.value = rows[i]['researcher/UserId'];
+ option.text = rows[i]['researcher/displayName'];
+ if (instrument === rows[i].Id) {
+ option.selected = true;
+ }
+ instrumentOperatorDropDown.appendChild(option);
}
- instrumentOperatorDropDown += ' ';
- jQuery('#instrumentOperatorDropDown').html(instrumentOperatorDropDown);
}
});
- function stringToColor(color) {
- // Create a temporary DOM element
- const tempElement = document.createElement('div');
- tempElement.style.color = color;
- document.body.appendChild(tempElement);
-
- // Get the computed color value
- const computedColor = window.getComputedStyle(tempElement).color;
- document.body.removeChild(tempElement);
-
- // Convert RGB to HEX
- const rgbValues = computedColor.match(/\d+/g).map(Number);
- if (rgbValues.length === 3) {
- const [r, g, b] = rgbValues;
- return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
- }
-
- return null; // Return null if conversion fails
- }
-
- function getContrastTextColor(hexcolor){
- var r = parseInt(hexcolor.substring(1,3),16);
- var g = parseInt(hexcolor.substring(3,5),16);
- var b = parseInt(hexcolor.substring(5,7),16);
- var yiq = ((r*299)+(g*587)+(b*114))/1000;
- return (yiq >= 128) ? 'black' : 'white';
- }
function editEvent(event) {
let startDate = event.startDate;
let endDate = event.endDate;
- if (!event.id) {
- // for new events subtract 1 minute from the end date to show the end date in the date picker
- endDate = new Date(endDate.getTime() - 60000);
+
+ if (!event.id && startDate.getHours() === 0 && startDate.getMinutes() === 0 && endDate.getHours() === 0 && endDate.getMinutes() === 0) {
+ // Default to starting at 8AM and ending at 5PM
+ startDate.setHours(8);
+ endDate.setHours(17);
+ endDate.setDate(endDate.getDate() - 1);
}
let startDateFormatted = DateFormat.format.date(startDate, LABKEY.container.formats.dateTimeFormat);
@@ -409,7 +412,7 @@ Time Scheduled
$('#event-modal input[name="event-name"]').val(event ? event.name : '');
$('#event-modal input[name="event-notes"]').val(event ? event.notes : '');
$('#delete-event').toggle(!!event.id);
- $('#add-event').text(event.id ? 'Edit' : 'Add');
+ $('#add-event').text('Save');
$('#event-modal').modal();
}
@@ -424,10 +427,10 @@ Time Scheduled
eventToSave.startTime = $('#event-modal input[name="event-start-date"]').val();
eventToSave.endTime = $('#event-modal input[name="event-end-date"]').val();
eventToSave.project = project;
- eventToSave.instrument = instrument ? instrument : $('#instrumentDropDown select[name="instrumentDropDown"]').val();
+ eventToSave.instrument = instrument ? instrument : $('#instrumentDropDown').val();
eventToSave.name = $('#event-modal input[name="event-name"]').val();
eventToSave.notes = $('#event-modal input[name="event-notes"]').val();
- eventToSave.instrumentoperator = $('#instrumentOperatorDropDown select[name="instrumentOperatorDropDown"]').val();
+ eventToSave.instrumentoperator = $('#instrumentOperatorDropDown').val();
if (eventToSave.endTime < eventToSave.startTime) {
alert('End date must be after start date');
@@ -437,7 +440,7 @@ Time Scheduled
const events = calendar.getEvents();
// validate that the events time do not overlap with existing events
for (let i = 0; i < events.length; i++) {
- if (eventToSave.id != events[i].id) {
+ if (eventToSave.id !== events[i].id) {
let newEvent = {
start: new Date(eventToSave.startTime),
end: new Date(eventToSave.endTime)
@@ -685,12 +688,13 @@ Time Scheduled
function loadInstrumentSchedule(callback) {
let instrumentSchedule = [];
+ const currentInstrument = instrument ? instrument : parseInt($('#instrumentDropDown').val());
LABKEY.Query.selectRows({
schemaName: 'targetedms',
queryName: 'instrumentSchedule',
- columns: 'Id,startTime,endTime,name,notes,instrument/color,project/Id, project/title',
+ columns: 'Id,startTime,endTime,name,notes,instrument/color,instrument/Id,project/Id, project/Title',
filterArray: [
- LABKEY.Filter.create('instrument', instrument ? instrument : $('#instrumentDropDown select[name="instrumentDropDown"]').val()),
+ LABKEY.Filter.create('instrument', instrument ? instrument : $('#instrumentDropDown').val()),
],
success: function (data) {
let rows = data.rows;
@@ -704,7 +708,7 @@ Time Scheduled
notes: row.notes,
color: row['instrument/color'],
project: row['project/Id'],
- title: row['project/title']
+ title: row['project/Title']
});
}
callback(instrumentSchedule);
@@ -728,10 +732,6 @@ Time Scheduled
deleteEvent();
});
- $('#event-delete').click(function () {
- deleteEvent();
- });
-
$('#close-schedule').click(function () {
$('#schedule-save-error').text('');
$('#event-modal').modal('hide');
@@ -745,8 +745,7 @@ Time Scheduled
addPaymentMethodEl.addEventListener('click', function () {
paymentMethodCount++;
let id = 'paymentMethodDropDown' + paymentMethodCount;
- let paymentMethodDropDown = 'Payment Method: ';
- paymentMethodDropDown += '';
+ let paymentMethodDropDown = '';
for (let i = 0; i < paymentMethodsData.length; i++) {
paymentMethodDropDown += '' + LABKEY.Utils.encodeHtml(paymentMethodsData[i].name) + ' ';
}
@@ -772,17 +771,16 @@ Time Scheduled
});
// refresh the page on project change
- document.getElementById('projectDropDown').addEventListener('change', function (event) {
- document.location.href = LABKEY.ActionURL.buildURL('targetedms', 'scheduleInstrument', null, {project: event.target.value});
-
+ document.getElementById('projectDropDown').addEventListener('change', function () {
+ document.location.href = LABKEY.ActionURL.buildURL('targetedms', 'scheduleInstrument', null, {project: document.getElementById('projectDropDown').value});
});
// refresh the page on instrument change
- document.getElementById('instrumentDropDown').addEventListener('change', function (event) {
- document.location.href = LABKEY.ActionURL.buildURL('targetedms', 'scheduleInstrument', null, {project: LABKEY.ActionURL.getParameter('project'), instrument: event.target.value});
+ document.getElementById('instrumentDropDown').addEventListener('change', function () {
+ document.location.href = LABKEY.ActionURL.buildURL('targetedms', 'scheduleInstrument', null, {project: LABKEY.ActionURL.getParameter('project'), instrument: document.getElementById('instrumentDropDown').value});
});
- document.getElementById('instrument-rates').setAttribute('href', LABKEY.ActionURL.buildURL('query', 'executeQuery', LABKEY.ActionURL.getContainer(), {schemaName: 'targetedms', 'query.queryName': 'InstrumentRate'}));
+ document.getElementById('instrument-rates').innerHTML = LABKEY.Utils.textLink({text: 'View the current rates for instruments', href: LABKEY.ActionURL.buildURL('query', 'executeQuery', LABKEY.ActionURL.getContainer(), {schemaName: 'targetedms', 'query.queryName': 'InstrumentRate'})});
});
diff --git a/resources/views/scheduleInstrument.view.xml b/resources/views/scheduleInstrument.view.xml
index c45623167..66bbcc043 100644
--- a/resources/views/scheduleInstrument.view.xml
+++ b/resources/views/scheduleInstrument.view.xml
@@ -3,5 +3,6 @@
+
\ No newline at end of file
diff --git a/src/org/labkey/targetedms/view/CrossLinkedPeptideInfo.java b/src/org/labkey/targetedms/view/CrossLinkedPeptideInfo.java
index 2293437be..c8774a0c4 100644
--- a/src/org/labkey/targetedms/view/CrossLinkedPeptideInfo.java
+++ b/src/org/labkey/targetedms/view/CrossLinkedPeptideInfo.java
@@ -189,12 +189,13 @@ public List findMatches(List proteins)
do
{
index = proteinSequence.indexOf(getUnmodified(), index + 1);
- if (index >= 0)
+ if (index != -1 &&
+ (index == 0 || proteinSequence.charAt(index - 1) == 'K' || proteinSequence.charAt(index - 1) == 'R'))
{
result.add(new Match(protein, index));
}
}
- while (index > 0);
+ while (index >= 0);
}
}
return result;
@@ -255,6 +256,37 @@ public void testOmittedIndices()
Assert.assertEquals("Link location", Set.of(9), i.getExtraSequences().get(0).getLinkIndices());
Assert.assertEquals("Link location", Set.of(4, 7, 0, 1), i.getExtraSequences().get(4).getLinkIndices());
}
+
+ @Test
+ public void testTrypticMatches()
+ {
+ CrossLinkedPeptideInfo info = new CrossLinkedPeptideInfo("PEPTIDE");
+ PeptideSequence seq = info.getBaseSequence();
+
+ // Single protein containing multiple occurrences of PEPTIDE:
+ // - at start (allowed)
+ // - preceded by A (disallowed)
+ // - preceded by K (allowed)
+ // - preceded by R (allowed)
+ Protein protein = new Protein();
+ String proteinSeq = "PEPTIDE" + "APEPTIDE" + "KPEPTIDE" + "RPEPTIDE";
+ protein.setSequence(proteinSeq);
+
+ List proteins = Arrays.asList(protein);
+ List matches = seq.findMatches(proteins);
+
+ // Expect three valid matches: indices 0 (start), 16 (after K), 24 (after R)
+ Assert.assertEquals("Should find three valid matches within a single protein sequence", 3, matches.size());
+
+ Assert.assertSame("First match should be at start (index 0)", protein, matches.get(0).protein());
+ Assert.assertEquals(0, matches.get(0).index());
+
+ Assert.assertSame("Second match should be after K at index 16", protein, matches.get(1).protein());
+ Assert.assertEquals(16, matches.get(1).index());
+
+ Assert.assertSame("Third match should be after R at index 24", protein, matches.get(2).protein());
+ Assert.assertEquals(24, matches.get(2).index());
+ }
}
public class Linker
diff --git a/test/sampledata/TargetedMS/CrosslinkPeptideMapTest.sky.zip b/test/sampledata/TargetedMS/CrosslinkPeptideMapTest.sky.zip
index 37bf5fc5b..287b2780f 100644
Binary files a/test/sampledata/TargetedMS/CrosslinkPeptideMapTest.sky.zip and b/test/sampledata/TargetedMS/CrosslinkPeptideMapTest.sky.zip differ
diff --git a/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java b/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java
new file mode 100644
index 000000000..f173a348d
--- /dev/null
+++ b/test/src/org/labkey/test/tests/targetedms/InstrumentSchedulingTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.labkey.remoteapi.CommandException;
+import org.labkey.remoteapi.query.InsertRowsCommand;
+import org.labkey.remoteapi.security.WhoAmICommand;
+import org.labkey.test.BaseWebDriverTest;
+import org.labkey.test.Locator;
+import org.labkey.test.TestTimeoutException;
+import org.labkey.test.util.APIContainerHelper;
+import org.labkey.test.util.ApiPermissionsHelper;
+import org.labkey.test.util.PermissionsHelper;
+import org.labkey.test.util.PortalHelper;
+import org.labkey.test.util.PostgresOnlyTest;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+
+@Category({})
+public class InstrumentSchedulingTest extends TargetedMSTest implements PostgresOnlyTest
+{
+ protected static final String SCHEDULER_USER_1 = "scheduler1@targetedms.test";
+ protected static final String SCHEDULER_USER_2 = "scheduler2@targetedms.test";
+
+ public static final String INSTRUMENT_1 = "Instrument1";
+ public static final String INSTRUMENT_2 = "Instrument2";
+ public static final String INACTIVE_INSTRUMENT = "InactiveInstrument";
+ public static final String PROJECT_1 = "Project1";
+ public static final String PROJECT_2 = "Project2";
+ public static final Locator.IdLocator EVENT_NAME_FIELD = Locator.id("event-name");
+ public static final Locator.IdLocator EVENT_NOTE_FIELD = Locator.id("event-notes");
+
+ @BeforeClass
+ public static void initProject() throws IOException, CommandException
+ {
+ InstrumentSchedulingTest init = getCurrentTest();
+ init.doInit();
+ }
+
+ private void doInit() throws IOException, CommandException
+ {
+ setupFolder(FolderType.Experiment);
+ new PortalHelper(this).addWebPart("Instrument Scheduling Admin");
+
+ int schedulerUser1Id = _userHelper.createUser(SCHEDULER_USER_1).getUserId();
+ int schedulerUser2Id = _userHelper.createUser(SCHEDULER_USER_2).getUserId();
+ ApiPermissionsHelper apiPermissionsHelper = new ApiPermissionsHelper(this);
+ apiPermissionsHelper.addMemberToRole(SCHEDULER_USER_1, "Editor", PermissionsHelper.MemberType.user);
+ apiPermissionsHelper.addMemberToRole(SCHEDULER_USER_2, "Editor", PermissionsHelper.MemberType.user);
+
+ InsertRowsCommand instrumentInsert = new InsertRowsCommand("targetedms", "msInstrument");
+ instrumentInsert.setRows(Arrays.asList(
+ Map.of("Name", INSTRUMENT_1, "Active", true, "Color", "#ee0000"),
+ Map.of("Name", INSTRUMENT_2, "Active", true, "Color", "#00ee00"),
+ Map.of("Name", INACTIVE_INSTRUMENT, "Active", false, "Color", "#0000ee")
+ ));
+ List> instruments = instrumentInsert.execute(createDefaultConnection(), getProjectName()).getRows();
+
+ InsertRowsCommand projectInsert = new InsertRowsCommand("targetedms", "msProject");
+ projectInsert.setRows(Arrays.asList(
+ Map.of("Affiliation", "LabKey", "Title", PROJECT_1, "SubmitDate", "1/1/2025", "CollaborationWith", "Mike", "ScientificQuestion", "Why do I have to enter this?", "abstract", "b"),
+ Map.of("Affiliation", "UW", "Title", PROJECT_2, "SubmitDate", "2/2/2025", "CollaborationWith", "Josh", "ScientificQuestion", "Why is the sky blue?", "abstract", "a")
+ ));
+ List> projects = projectInsert.execute(createDefaultConnection(), getProjectName()).getRows();
+
+ InsertRowsCommand rateTypeInsert = new InsertRowsCommand("targetedms", "rateType");
+ rateTypeInsert.setRows(Arrays.asList(
+ Map.of("Name", "DefaultRate", "SetupFee", 50),
+ Map.of("Name", "BigSpenderRate", "SetupFee", 50)
+ ));
+ List> rateTypes = rateTypeInsert.execute(createDefaultConnection(), getProjectName()).getRows();
+
+ InsertRowsCommand paymentMethodInsert = new InsertRowsCommand("targetedms", "paymentMethod");
+ paymentMethodInsert.setRows(Arrays.asList(
+ Map.of("UWBudgetNumber", "1111", "Name", "PaymentMethod1"),
+ Map.of("UWBudgetNumber", "2222", "Name", "PaymentMethod2")
+ ));
+ List> paymentMethods = paymentMethodInsert.execute(createDefaultConnection(), getProjectName()).getRows();
+
+ InsertRowsCommand projectPaymentMethodInsert = new InsertRowsCommand("targetedms", "projectPaymentMethod");
+ projectPaymentMethodInsert.setRows(Arrays.asList(
+ Map.of("PaymentMethod", paymentMethods.get(0).get("Id"), "Project", projects.get(0).get("Id")),
+ Map.of("PaymentMethod", paymentMethods.get(1).get("Id"), "Project", projects.get(1).get("Id"))
+ ));
+ List> projectPaymentMethods = projectPaymentMethodInsert.execute(createDefaultConnection(), getProjectName()).getRows();
+
+ int currentUserId = new WhoAmICommand().execute(createDefaultConnection(), getProjectName()).getUserId().intValue();
+
+ InsertRowsCommand projectResearcherInsert = new InsertRowsCommand("targetedms", "projectResearcher");
+ projectResearcherInsert.setRows(Arrays.asList(
+ Map.of("Project", projects.get(0).get("Id"), "Researcher", schedulerUser1Id),
+ Map.of("Project", projects.get(1).get("Id"), "Researcher", schedulerUser1Id),
+ Map.of("Project", projects.get(1).get("Id"), "Researcher", schedulerUser2Id),
+ Map.of("Project", projects.get(0).get("Id"), "Researcher", currentUserId),
+ Map.of("Project", projects.get(1).get("Id"), "Researcher", currentUserId)
+ ));
+ List> projectResearchers = projectResearcherInsert.execute(createDefaultConnection(), getProjectName()).getRows();
+
+ InsertRowsCommand instrumentRateInsert = new InsertRowsCommand("targetedms", "instrumentRate");
+ instrumentRateInsert.setRows(Arrays.asList(
+ Map.of("Instrument", instruments.get(0).get("Id"), "rateType", rateTypes.get(0).get("Id"), "fee", 100),
+ Map.of("Instrument", instruments.get(1).get("Id"), "rateType", rateTypes.get(1).get("Id"), "fee", 110)
+ ));
+ List> instrumentRates = instrumentRateInsert.execute(createDefaultConnection(), getProjectName()).getRows();
+ }
+
+ @Test
+ public void testSchedule()
+ {
+ goToProjectHome();
+ clickAndWait(Locator.linkWithText("Your project list"));
+ waitForText(PROJECT_1, PROJECT_2);
+ clickAndWait(Locator.linkWithText(PROJECT_1));
+ waitAndClickAndWait(Locator.linkWithText("Schedule instrument time"));
+
+ String yearMonth = Calendar.getInstance().get(Calendar.YEAR) + "-";
+ int month = (Calendar.getInstance().get(Calendar.MONTH) + 1);
+ if (month < 10)
+ {
+ yearMonth += yearMonth;
+ }
+ yearMonth += month;
+
+ scheduleInstrument(yearMonth + "-02");
+ scheduleInstrument(yearMonth + "-03");
+ scheduleInstrument(yearMonth + "-03", true);
+ scheduleInstrument(yearMonth + "-03");
+
+ assertProjectEventCounts(2, 0);
+
+ sleep(1000);
+ doAndWaitForPageToLoad(() -> selectOptionByText(Locator.id("projectDropDown"), PROJECT_2));
+
+ scheduleInstrument(yearMonth + "-04");
+ assertProjectEventCounts(1, 2);
+
+ scheduleInstrument(yearMonth + "-05");
+ assertProjectEventCounts(2, 2);
+
+ sleep(1000);
+ doAndWaitForPageToLoad(() -> selectOptionByText(Locator.id("instrumentDropDown"), INSTRUMENT_2));
+ scheduleInstrument(yearMonth + "-06");
+ assertProjectEventCounts(1, 0);
+
+ goToDashboard();
+ waitAndClickAndWait(Locator.linkWithText("All instrument calendar view"));
+ assertTextPresent(INSTRUMENT_1, INSTRUMENT_2, INACTIVE_INSTRUMENT);
+
+ assertProjectEventCounts(5, 0);
+
+ selectOptionByText(Locator.id("projectFilter"), PROJECT_2);
+ assertProjectEventCounts(3, 2);
+
+ goToDashboard();
+ waitAndClickAndWait(Locator.linkWithText("Instrument billing report"));
+ assertTextPresent("$950.00", 4);
+ assertTextPresent("$1,040.00", 1);
+
+ // Future test cases:
+ // Split payment across multiple methods
+ // Schedule for hours within a day instead of 24-hour periods
+ // Check billing for individual months, including reservations that span month boundaries with start/end dates
+ // Ensure that overlapping reservations are rejected
+ // Ensure that reservations cannot be made for inactive instruments
+ }
+
+ private void assertProjectEventCounts(int expectedActiveCount, int expectedOtherCount)
+ {
+ Locator activeLocator = Locator.byClass("activeProjectEvent");
+ Locator otherLocator = Locator.byClass("otherProjectEvent");
+ if (expectedActiveCount > 0)
+ {
+ waitForElementToBeVisible(activeLocator);
+ }
+ if (expectedOtherCount > 0)
+ {
+ waitForElementToBeVisible(otherLocator);
+ }
+ assertElementPresent(activeLocator, expectedActiveCount);
+ assertElementPresent(otherLocator, expectedOtherCount);
+ }
+
+ private void scheduleInstrument(String yearMonthDay)
+ {
+ scheduleInstrument(yearMonthDay, false);
+ }
+
+ private void scheduleInstrument(String yearMonthDay, boolean delete)
+ {
+ waitAndClick(Locator.tagWithAttribute("td", "data-date", yearMonthDay));
+ waitForText("Add Instrument Time");
+ if (delete)
+ {
+ waitAndClick(Locator.button("Delete"));
+ }
+ else
+ {
+ waitForElementToBeVisible(EVENT_NAME_FIELD);
+ setFormElement(EVENT_NAME_FIELD.findElement(getDriver()), "A name!");
+ setFormElement(EVENT_NOTE_FIELD.findElement(getDriver()), "A note!");
+ waitAndClick(Locator.button("Save"));
+ waitAndClick(Locator.button("Yes"));
+ }
+ }
+
+ @Override
+ protected void doCleanup(boolean afterTest) throws TestTimeoutException
+ {
+ // these tests use the UIContainerHelper for project creation, but we can use the APIContainerHelper for deletion
+ APIContainerHelper apiContainerHelper = new APIContainerHelper(this);
+ apiContainerHelper.deleteProject(getProjectName(), afterTest);
+
+ _userHelper.deleteUsers(false, SCHEDULER_USER_1);
+ _userHelper.deleteUsers(false, SCHEDULER_USER_2);
+ }
+}
diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSMAMTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSMAMTest.java
index 01f5c0a4b..c7f393179 100644
--- a/test/src/org/labkey/test/tests/targetedms/TargetedMSMAMTest.java
+++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSMAMTest.java
@@ -19,6 +19,8 @@
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.labkey.test.Locator;
+import org.labkey.test.util.DataRegion;
+import org.labkey.test.util.DataRegionTable;
import static org.junit.Assert.assertTrue;
@@ -56,10 +58,13 @@ public void testSteps()
assertTextPresent("Chromatograms");
clickAndWait(Locator.linkContainingText("Peptide Map"));
+ DataRegionTable table = new DataRegionTable("PeptideIds", getDriver());
+ table.setPageSize(250);
assertTextPresentInThisOrder("11.3", "14.1", "14.8");
assertTextPresentInThisOrder("1501.75", "1078.50", "1547.71");
- assertTextPresentInThisOrder("NU205", "NU205", "1433Z", "UCRI; RL35");
- assertTextPresentInThisOrder("70-84", "325-333", "28-41", "190-196; 26-32");
+ assertTextPresentInThisOrder("NU205", "1433Z", "RL35", "HSP72; HSP7C");
+ assertTextNotPresent("UCRI; RL35"); // Ensure we don't have non-tryptic matches anymore
+ assertTextPresentInThisOrder("70-84", "325-333", "28-41", "305-314; 302-311");
assertTextPresentInThisOrder("(K)ASTEGVAIQGQQGTR(L)", "(K)AQYEDIANR(S)", "(K)SVTEQGAELSNEER(N)");
assertTextPresentInThisOrder("Carbamidomethyl Cysteine @ C157", "Carbamidomethyl Cysteine @ C245", "Carbamidomethyl Cysteine @ C94");
@@ -75,15 +80,15 @@ public void testCrossLinkedPeptideMap()
clickAndWait(Locator.linkContainingText("Panorama Dashboard"));
clickAndWait(Locator.linkContainingText(CROSS_LINKED_SKY_FILE));
- verifyRunSummaryCountsPep(2,3,0, 3,3, 1, 0, 0);
+ verifyRunSummaryCountsPep(2,2,0, 2,2, 1, 0, 0);
clickAndWait(Locator.linkContainingText("Peptide Map"));
- assertTextPresentInThisOrder("364-366", "367-369", "364-367");
+ assertTextPresentInThisOrder("121-124", "342-345", "142-145");
// Disulfide bonds
- assertTextPresentInThisOrder("Q364-T369-D364/\nN366-T369-D364", "V121-S345-Q142/\nQ124-S345-Q142");
- assertTextPresentInThisOrder("(A)LKPLALV(D)", "(G)AVVQDPA(Y)", "(F)YGEATSR(E)");
+ assertTextPresentInThisOrder("V121-S345-Q142/\nQ124-S345-Q142", "L11-A137-Y271/\nL11-A137-Y271/\nV17-A137-Y271/");
+ assertTextPresentInThisOrder("(K)LKPLALV(D)", "(K)AVVQDPA(Y)", "(R)YGEATSR(E)");
// Ensure that the highlighting is as expected for both crosslinking and modification
- assertTrue(getHtmlSource().contains("(Y)Q M N (D)"));
+ assertTrue(getHtmlSource().contains("(R)V SSQ (Q)"));
}
}
diff --git a/webapp/TargetedMS/js/scheduleUtils.js b/webapp/TargetedMS/js/scheduleUtils.js
new file mode 100644
index 000000000..c1f92538c
--- /dev/null
+++ b/webapp/TargetedMS/js/scheduleUtils.js
@@ -0,0 +1,58 @@
+// Shared utilities for TargetedMS scheduling pages
+// Expose on a namespaced object to avoid globals
+(function(window) {
+ const utils = {};
+
+ // Convert a CSS color string (named, rgb, hex) to standard 6-digit HEX color (#RRGGBB)
+ utils.stringToColor = function(color) {
+ if (!color) return '#888888';
+ try {
+ const tempElement = document.createElement('div');
+ tempElement.style.color = color;
+ document.body.appendChild(tempElement);
+ const computedColor = window.getComputedStyle(tempElement).color;
+ document.body.removeChild(tempElement);
+ const rgbValues = computedColor.match(/\d+/g);
+ if (rgbValues && rgbValues.length >= 3) {
+ const r = parseInt(rgbValues[0], 10);
+ const g = parseInt(rgbValues[1], 10);
+ const b = parseInt(rgbValues[2], 10);
+ return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
+ .toString(16)
+ .slice(1)
+ .toUpperCase();
+ }
+ } catch (e) {
+ // fall through to default return
+ }
+ return '#888888';
+ };
+
+ // Pick black or white text for best contrast over a given hex background color
+ utils.getContrastTextColor = function(hexColor) {
+ if (!hexColor) return '#000000';
+ const c = hexColor.replace('#', '');
+ if (c.length !== 6) return '#000000';
+ const r = parseInt(c.substring(0, 2), 16);
+ const g = parseInt(c.substring(2, 4), 16);
+ const b = parseInt(c.substring(4, 6), 16);
+ const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
+ return yiq >= 128 ? '#000000' : '#FFFFFF';
+ };
+
+ // Return a time-only format string without seconds/millis based on the container setting
+ utils.getTimeOnlyFormat = function() {
+ if (window.LABKEY && LABKEY.container?.formats?.timeFormat) {
+ return LABKEY.container.formats.timeFormat.replace(':ss', '').replace('.SSS', '');
+ }
+ return 'HH:mm';
+ };
+
+ // Format a time range using DateFormat and time-only format
+ utils.formatTimeRange = function(start, end) {
+ const fmt = utils.getTimeOnlyFormat();
+ return DateFormat.format.date(start, fmt) + ' - ' + DateFormat.format.date(end, fmt);
+ };
+
+ window.ScheduleUtils = utils;
+})(window);