From ea161792ac7858dc43cde993d004102ab7985c47 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Thu, 24 Sep 2026 06:14:09 -0700 Subject: [PATCH 1/2] Remove chargeRates debug logging and an unclosed security escalation (#1047) ## Rationale This removes a debug log line that floods the site logs, and deletes an unused method that would leak an EHR security escalation if anything ever called it. The chargeRates trigger logs every group/category association row on each write batch, so charge-rate uploads bury the log. `checkIfUserIsWaterAdmin` began an EHR escalation without closing it, which would leave the request thread escalated and skip the audit entry. ## Related Pull Requests - https://github.com/LabKey/platform/pull/8079 ## Changes - Removed the debug logging from the chargeRates trigger's association cache. - Removed the unused water-admin check that opened an escalation it never closed. --- .../labkey/wnprc_ehr/TriggerScriptHelper.java | 23 ------------------- .../queries/ehr_billing/chargeRates.js | 2 -- 2 files changed, 25 deletions(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java index f33aa2e47..12503ebc1 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/TriggerScriptHelper.java @@ -2616,29 +2616,6 @@ public boolean checkAnimalRequestExists(Integer rowid) return requests.length() > 0; } - public Boolean checkIfUserIsWaterAdmin(int userId, int project, User currentUser) - { - boolean returnCondition = false; - - TableInfo waterOrdersAccess = getTableInfo("wnprc","watermonitoring_access"); - SimpleFilter filter = new SimpleFilter(FieldKey.fromString("alloweduser/UserId"), userId); - filter.addCondition(FieldKey.fromString("project"), project,CompareType.EQUAL); - - TableSelector userList = new TableSelector(waterOrdersAccess, PageFlowUtil.set("date", "alloweduser", "project"),filter, null); - userList.setMaxRows(1); - Map[] userFromServer = userList.getMapArray(); - - //updating and adding waters from server, objectid will take are of any duplicates - - if (userFromServer.length>0){ - EHRSecurityEscalator.beginEscalation(currentUser,container,"Allowing user to modify Water Orders"); - - returnCondition = true; - - } - return returnCondition; - } - public void setAliasRow(JSONArray alias) { _aliasRow = alias; diff --git a/wnprc_billing/resources/queries/ehr_billing/chargeRates.js b/wnprc_billing/resources/queries/ehr_billing/chargeRates.js index 8213e02a9..6c19ce47f 100644 --- a/wnprc_billing/resources/queries/ehr_billing/chargeRates.js +++ b/wnprc_billing/resources/queries/ehr_billing/chargeRates.js @@ -48,8 +48,6 @@ function onInit(event, helper){ var chargeCategoryId = row["chargeCategoryId"]["value"]; var chargeCategoryAssoc = chargeGroupName + ", " + chargeCategoryId; groupCategoryAssociations[chargeCategoryAssoc] = chargeCategoryAssoc; - - console.log("groupCategoryAssociations[chargeCategoryAssoc] = " + groupCategoryAssociations[chargeCategoryAssoc]); } }, failure: function (error) { From 1bd83529f6f5e9c8846d99203850b44ea276d974 Mon Sep 17 00:00:00 2001 From: Josh Eckels Date: Thu, 24 Sep 2026 17:19:40 -0700 Subject: [PATCH 2/2] Optimize Necropsy Schedule (#1048) ## Rationale When users rapidly page through months in Necropsy Schedule, it issues separate and moderately expensive queries for each month. That's fine if they only page a few months or page slowly. If they page 30 months with repeated clicks, it's a lot of load for the DB. ## Changes - Switch to pull the full schedule from study.necropsy, which can return all of the needed data for the calendar view in < 1 second - Fetch details for the panel on demand when the user clicks to view the details - Optimize Necropsy Schedule joins single-row filtered selects. Neutral on performance when queried without filters. - Enable paging by year or showing the full year to improve UX --- .../queries/study/Necropsy Schedule.sql | 91 ++++++---------- .../pages/dataentry/NecropsySchedule.jsp | 100 +++++++++++------- 2 files changed, 94 insertions(+), 97 deletions(-) diff --git a/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql b/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql index 44871fc77..e9ce8d028 100644 --- a/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql +++ b/WNPRC_EHR/resources/queries/study/Necropsy Schedule.sql @@ -1,10 +1,19 @@ /* This query feeds the Necropsy Schedule JSP page/calendar. */ +/* Correlated subqueries rather than lookups/joins that aggregate entire datasets, so cost scales with the rows returned. */ SELECT lsid - ,necropsy.taskid + ,taskid ,animalid ,animalid.Demographics.gender AS sex ,animalid.age.ageFriendly AS age - ,animalid.mostRecentWeight.mostRecentWeight AS weight + ,CAST((SELECT ROUND(CAST(AVG(w.weight) AS DOUBLE), 2) + FROM study.weight w + WHERE w.Id = necropsy.animalid + AND w.qcstate.publicdata = TRUE + AND w.date = (SELECT MAX(w2.date) + FROM study.weight w2 + WHERE w2.Id = necropsy.animalid + AND w2.qcstate.publicdata = TRUE + AND w2.weight IS NOT NULL)) AS DOUBLE) AS weight ,animalid.Demographics.medical AS medical ,animalid.curLocation.room AS cur_room ,animalid.curLocation.cage AS cur_cage @@ -21,32 +30,24 @@ SELECT lsid ,delivery_option.title AS who_delivers ,shipping_comment AS delivery_comment ,qcstate - ,animalid.Demographics.necropsyAbstractNotes.remark AS remark - ,CASE - WHEN hasTissuesForAvrl IS NULL - THEN FALSE - ELSE TRUE - END AS has_tissues_for_avrl - ,CASE - WHEN hasTissuesForWimr IS NULL - THEN FALSE - ELSE TRUE - END AS has_tissues_for_wimr - ,CASE - WHEN hasTissuesForCcourt IS NULL - THEN FALSE - ELSE TRUE - END AS has_tissues_for_ccourt - ,CASE - WHEN hasTissuesForBmq IS NULL - THEN FALSE - ELSE TRUE - END AS has_tissues_for_bmq - ,CASE - WHEN hasTissuesForElements IS NULL - THEN FALSE - ELSE TRUE - END AS has_tissues_for_elements + ,(SELECT GROUP_CONCAT((a.remark || ' (' || COALESCE(CAST(a.project AS VARCHAR), 'no proj.') || ')'), '; ') as x + FROM study.NecropsyAbstract a + WHERE a.Id = necropsy.animalid) AS remark + ,CASE WHEN EXISTS (SELECT 1 FROM study.tissue_samples t WHERE t.taskid = necropsy.lsid + AND t.ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_AVRL')) + THEN TRUE ELSE FALSE END AS has_tissues_for_avrl + ,CASE WHEN EXISTS (SELECT 1 FROM study.tissue_samples t WHERE t.taskid = necropsy.lsid + AND t.ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_WIMR')) + THEN TRUE ELSE FALSE END AS has_tissues_for_wimr + ,CASE WHEN EXISTS (SELECT 1 FROM study.tissue_samples t WHERE t.taskid = necropsy.lsid + AND t.ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_CCOURT')) + THEN TRUE ELSE FALSE END AS has_tissues_for_ccourt + ,CASE WHEN EXISTS (SELECT 1 FROM study.tissue_samples t WHERE t.taskid = necropsy.lsid + AND t.ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_BMQ')) + THEN TRUE ELSE FALSE END AS has_tissues_for_bmq + ,CASE WHEN EXISTS (SELECT 1 FROM study.tissue_samples t WHERE t.taskid = necropsy.lsid + AND t.ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_ELEMENTS')) + THEN TRUE ELSE FALSE END AS has_tissues_for_elements ,state FROM (SELECT taskid AS lsid ,taskid.rowid AS taskid @@ -62,42 +63,10 @@ SELECT lsid ,shipping_comment ,location ,performedby - ,qcstate.label as qcstate + ,qcstate.label AS qcstate ,taskid.qcstate AS state FROM study.necropsy WHERE taskid IS NOT NULL) necropsy /* Look up the display friendly name for the delivery option. */ LEFT JOIN wnprc.necropsy_delivery_options delivery_option ON necropsy.shipping = delivery_option.key -/* Flag necropsies that have tissues that need to be couriered to AVRL. */ - LEFT JOIN (SELECT taskid - ,TRUE AS hasTissuesForAvrl - FROM tissue_samples - WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_AVRL') -- 'COURIER_AVRL' - GROUP BY taskid) avrl_tissues - ON necropsy.lsid = avrl_tissues.taskid -/* Flag necropsies that have tissues that need to be couriered to WIMR. */ - LEFT JOIN (SELECT taskid - ,TRUE AS hasTissuesForWimr - FROM tissue_samples - WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_WIMR') -- 'COURIER_WIMR' - GROUP BY taskid) wimr_tissues - ON necropsy.lsid = wimr_tissues.taskid - LEFT JOIN (SELECT taskid - ,TRUE AS hasTissuesForCcourt - FROM tissue_samples - WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_CCOURT') -- 'COURIER_WIMR' - GROUP BY taskid) ccourt_tissues - ON necropsy.lsid = ccourt_tissues.taskid - LEFT JOIN (SELECT taskid - ,TRUE AS hasTissuesForBmq - FROM tissue_samples - WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_BMQ') -- 'COURIER_WIMR' - GROUP BY taskid) bmq_tissues - ON necropsy.lsid = bmq_tissues.taskid - LEFT JOIN (SELECT taskid - ,TRUE AS hasTissuesForElements - FROM tissue_samples - WHERE ship_to = javaConstant('org.labkey.wnprc_ehr.schemas.SqlQueryReferencePoints.COURIER_TO_ELEMENTS') -- 'COURIER_WIMR' - GROUP BY taskid) elements_tissues - ON necropsy.lsid = elements_tissues.taskid diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp index 2ced886e9..3b2823f12 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/pages/dataentry/NecropsySchedule.jsp @@ -334,57 +334,84 @@ var necropsySuiteLookup = <%=necropsySuiteLookup%>; WebUtils.VM.necropsySuiteLookup = necropsySuiteLookup; + // Fetch every necropsy once: a query per calendar view let rapid paging pile up concurrent DB queries. + // Reads the dataset directly for just what the calendar renders; the detail panel queries "Necropsy Schedule" on click. + var necropsyEventsPromise = null; + var loadNecropsyEvents = function() { + if (necropsyEventsPromise === null) { + necropsyEventsPromise = WebUtils.API.selectRows("study", "necropsy", { + columns: ['taskid', 'Id', 'date', 'location', 'QCState/Label'], + 'taskid~isnonblank': '', + maxRows: -1 + }).then(function(data) { + return data.rows.map(function(row) { + var eventObj = { + title: row.Id, + start: row.date, + startDate: new Date(row.date), + lsid: row.taskid, + display: 'block' + }; + + if (row.location in necropsySuiteLookup) { + eventObj.color = necropsySuiteLookup[row.location].color; + } + + if (row['QCState/Label'] == "Request: On Hold"){ + eventObj.color = "purple" + } + + return eventObj; + }); + }); + necropsyEventsPromise.catch(function() { + necropsyEventsPromise = null; + }); + } + return necropsyEventsPromise; + }; + + var selectedLsid = null; + $(document).ready(function() { let calendarEl = document.getElementById('calendar'); calendar = new FullCalendar.Calendar(calendarEl, { themeSystem: 'bootstrap', height: 800, initialView: 'dayGridMonth', + navLinks: true, headerToolbar: { - left: 'prev,next,today', + left: 'prevYear,prev,next,nextYear today', center: 'title', - right: 'dayGridMonth,timeGridWeek,timeGridDay' + right: 'multiMonthYear,dayGridMonth,timeGridWeek,timeGridDay' }, eventSources: [{ - events: function (fetchInfo, callback) { - console.log(" startStr " + fetchInfo.startStr); - console.log(" endtStr " + moment(fetchInfo.startStr).format( "YYYY-MM-DD")); + events: function (fetchInfo, successCallback, failureCallback) { + loadNecropsyEvents().then(function(events) { + successCallback(events.filter(function(eventObj) { + return eventObj.startDate >= fetchInfo.start && eventObj.startDate < fetchInfo.end; + })); + }, failureCallback); + }}], + eventClick: function(calEvent, jsEvent, view) { + var lsid = calEvent.event.extendedProps.lsid; + selectedLsid = lsid; WebUtils.API.selectRows("study", "Necropsy Schedule", { - "date~gte": moment(fetchInfo.startStr).format( "YYYY-MM-DD"), - "date~lte": moment(fetchInfo.endStr).format( "YYYY-MM-DD") + "lsid~eq": lsid }).then(function(data) { - var events = data.rows; - - callback(events.map(function(row) { - var eventObj = { - title: row.animalid, - start: row.date, - rawRowData: row, - display: 'block' - }; - - if (row.location in necropsySuiteLookup) { - eventObj.color = necropsySuiteLookup[row.location].color; - } - debugger; - - if (row.qcstate == "Request: On Hold"){ - eventObj.color = "purple" - } + if (selectedLsid !== lsid || data.rows.length === 0) { + return; + } - return eventObj; - })) - }) - }}], - eventClick: function(calEvent, jsEvent, view) { - jQuery.each(calEvent.event.extendedProps.rawRowData, function(key, value) { - if (key in WebUtils.VM.taskDetails) { - if (key == "date") { - value = displayDate(value); + jQuery.each(data.rows[0], function(key, value) { + if (key in WebUtils.VM.taskDetails) { + if (key == "date") { + value = displayDate(value); + } + WebUtils.VM.taskDetails[key](value); } - WebUtils.VM.taskDetails[key](value); - } + }); }); } },); @@ -704,6 +731,7 @@ ]); }).then(function() { // Refresh the calendar view. + necropsyEventsPromise = null; calendar.refetchEvents(); WebUtils.VM.pendingRequestTable.rows.remove(WebUtils.VM.requestRowInForm);