From ae2566943720d9c1edbdd6fc92c5a657542fc782 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Fri, 26 Sep 2025 17:06:40 -0500 Subject: [PATCH 01/12] Started form validation for the anesthesia recovery dataset. --- .../labkey/wnprc_ehr/WNPRC_EHRController.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index aa818eb9b..e13642dff 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -142,6 +142,8 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import org.labkey.api.action.SimpleApiJsonForm; +import org.springframework.validation.Errors; import static java.time.temporal.ChronoUnit.DAYS; @@ -2453,4 +2455,49 @@ public Object execute(NecropsyEditRequestNotificationForm form, BindException er } } + + public void validateAnesthesiaRecovery(SimpleApiJsonForm form, Errors errors) { + + // Verifies passed-in arguments are not null. + if (form.getJsonObject() == null) { + errors.reject(ERROR_MSG, "JSON argument cannot be null."); + return; + } + JSONObject myForm = form.getJsonObject(); + + // Retrieves the passed-in arguments. + JSONObject recordId = myForm.getJSONObject("Id"); + JSONObject recordRoom = myForm.getJSONObject("room"); + JSONObject recordDate = myForm.getJSONObject("date"); + JSONObject recordObservation = myForm.getJSONObject("observation"); + JSONObject recordRecoveryStart = myForm.getJSONObject("recoveryStart"); + JSONObject recordObserverComments = myForm.getJSONObject("observerComments"); + JSONObject recordObserver = myForm.getJSONObject("observer"); + JSONObject recordRecoveryId = myForm.getJSONObject("recoveryId"); + + _log.info("TEST MESSAGE:" + + "ID: " + recordId + + "ROOM: " + recordRoom + + "DATE: " + recordDate + + "OBSERVATION: " + recordObservation + + "RECOVERY START: " + recordRecoveryStart + + "OBSERVER COMMENTS: " + recordObserverComments + + "OBSERVER: " + recordObserver + + "RECOVERY ID: " + recordRecoveryId + ); + + + + +// public boolean isAliveAndAtCenter = false; +// +// private String animalid; +// +// public String getAnimalid() {return animalid;} +// +// public void setAnimalid(String animalid) {this.animalid = animalid;} +// +// public void setIsAliveAndAtCenter(boolean checkAlive) {this.isAliveAndAtCenter = checkAlive;} + } + } From 7ef141943ea11e363cd60266f4411a74298d3bf0 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Mon, 6 Oct 2025 16:23:10 -0500 Subject: [PATCH 02/12] Updated WNPRC_EHRController with anesthesia recovery validation function. --- .../labkey/wnprc_ehr/WNPRC_EHRController.java | 159 ++++++++++++++---- 1 file changed, 123 insertions(+), 36 deletions(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index e13642dff..59ff8b8ae 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -54,6 +54,7 @@ import org.labkey.api.ehr.EHRService; import org.labkey.api.ehr.demographics.AnimalRecord; import org.labkey.api.exp.property.Domain; +import org.labkey.api.ldk.notification.Notification; import org.labkey.api.module.Module; import org.labkey.api.module.ModuleLoader; import org.labkey.api.module.ModuleProperty; @@ -117,6 +118,7 @@ import org.labkey.wnprc_ehr.dataentry.validators.exception.InvalidAnimalIdException; import org.labkey.wnprc_ehr.dataentry.validators.exception.InvalidProjectException; import org.labkey.wnprc_ehr.notification.NecropsyEditRequestNotification; +import org.labkey.wnprc_ehr.notification.NotificationToolkit; import org.labkey.wnprc_ehr.schemas.WNPRC_Schema; import org.labkey.wnprc_ehr.service.dataentry.BehaviorDataEntryService; import org.springframework.validation.BindException; @@ -2455,49 +2457,134 @@ public Object execute(NecropsyEditRequestNotificationForm form, BindException er } } + @RequiresLogin +// @RequiresNoPermission + public static class UpdateAnesthesiaRecoveryDatasetAction extends MutatingApiAction { - public void validateAnesthesiaRecovery(SimpleApiJsonForm form, Errors errors) { +// @Override +// public void validateForm(SimpleApiJsonForm form, Errors errors) { +// +// // Verifies passed-in arguments are not null. +// if (form.getJsonObject() == null) { +// errors.reject(ERROR_MSG, "JSON argument cannot be null."); +// return; +// } +// JSONObject myForm = form.getJsonObject(); +// _log.info("TEST MESSAGE: JSON argument is: " + myForm); +// +// // Retrieves the passed-in arguments. +// String recordId = myForm.get("Id").toString(); +// String recordRoom = myForm.get("room").toString(); +// String recordDate = myForm.get("date").toString(); +// String recordObservation = myForm.get("observation").toString(); +// String recordRecoveryStart = myForm.get("recoveryStart").toString(); +// String recordObserverComments = myForm.get("observerComments").toString(); +// String recordObserver = myForm.get("observer").toString(); +// String recordRecoveryId = myForm.get("recoveryId").toString(); +// +// _log.info("TEST MESSAGE: ANESTHESIA VALIDATION 1"); +// +// _log.info("TEST MESSAGE 3:" + +// "ID: " + recordId + +// "ROOM: " + recordRoom + +// "DATE: " + recordDate + +// "OBSERVATION: " + recordObservation + +// "RECOVERY START: " + recordRecoveryStart + +// "OBSERVER COMMENTS: " + recordObserverComments + +// "OBSERVER: " + recordObserver + +// "RECOVERY ID: " + recordRecoveryId +// ); +// +// AnimalVerifier avrh1234 = new AnimalVerifier("rh1234", getUser(), getContainer()); // Does not exist +// AnimalVerifier avc19006 = new AnimalVerifier("c19006", getUser(), getContainer()); // Dead +// AnimalVerifier avc19007 = new AnimalVerifier("c19007", getUser(), getContainer()); // Alive +// +// AnimalVerifier av = new AnimalVerifier("rh1234", getUser(), getContainer()); // Alive +// +// +// // TODO: Validate data here. +// +// +// +// +//// public boolean isAliveAndAtCenter = false; +//// +//// private String animalid; +//// +//// public String getAnimalid() {return animalid;} +//// +//// public void setAnimalid(String animalid) {this.animalid = animalid;} +//// +//// public void setIsAliveAndAtCenter(boolean checkAlive) {this.isAliveAndAtCenter = checkAlive;} +// } - // Verifies passed-in arguments are not null. - if (form.getJsonObject() == null) { - errors.reject(ERROR_MSG, "JSON argument cannot be null."); - return; - } - JSONObject myForm = form.getJsonObject(); + @Override + public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception { + // Creates function variables. + BatchValidationException batchErrors = new BatchValidationException(); + ApiSimpleResponse response = new ApiSimpleResponse(); + NotificationToolkit notificationToolkit = new NotificationToolkit(); + _log.info("Started update to the anesthesia recovery dataset."); - // Retrieves the passed-in arguments. - JSONObject recordId = myForm.getJSONObject("Id"); - JSONObject recordRoom = myForm.getJSONObject("room"); - JSONObject recordDate = myForm.getJSONObject("date"); - JSONObject recordObservation = myForm.getJSONObject("observation"); - JSONObject recordRecoveryStart = myForm.getJSONObject("recoveryStart"); - JSONObject recordObserverComments = myForm.getJSONObject("observerComments"); - JSONObject recordObserver = myForm.getJSONObject("observer"); - JSONObject recordRecoveryId = myForm.getJSONObject("recoveryId"); + // Verifies passed-in arguments are not null. + if (form.getJsonObject() == null) { + response.put("detailedResponse", "JSON argument cannot be null."); + response.put("success", false); + return response; + } - _log.info("TEST MESSAGE:" + - "ID: " + recordId + - "ROOM: " + recordRoom + - "DATE: " + recordDate + - "OBSERVATION: " + recordObservation + - "RECOVERY START: " + recordRecoveryStart + - "OBSERVER COMMENTS: " + recordObserverComments + - "OBSERVER: " + recordObserver + - "RECOVERY ID: " + recordRecoveryId - ); + // Retrieves passed-in arguments. + JSONObject myForm = form.getJsonObject(); + String recordId = myForm.get("Id").toString(); + String recordObservation = myForm.get("observation").toString(); + + // Retrieves all necessary data. + try { + // Gets animal demographics record. + SimpleFilter demographicsFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); + String[] demographicsTargetColumns = new String[]{"Id", "calculated_status"}; + ArrayList> demographicsRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", demographicsFilter, null, demographicsTargetColumns); + if (!demographicsRows.isEmpty()) { + if (!demographicsRows.get(0).get("calculated_status").equals("Alive")) { + response.put("detailedResponse", "Animal " + recordId + " is not currently alive at the center."); + response.put("success", false); + return response; + } + } + // Gets all recoveries started. + SimpleFilter recoveryStartFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); + recoveryStartFilter.addCondition("observation", "Started Recovery", CompareType.EQUAL); + String[] recoveryStartTargetColumn = new String[]{"Id"}; + ArrayList> recoveryStartRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryStartFilter, null, recoveryStartTargetColumn); + // Gets all recoveries finished. + SimpleFilter recoveryEndFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); + recoveryEndFilter.addCondition("observation", "Fully Recovered", CompareType.EQUAL); + String[] recoveryEndTargetColumn = new String[]{"Id"}; + ArrayList> recoveryEndRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryEndFilter, null, recoveryEndTargetColumn); + // Verifies all recoveries have been closed before a new recovery can be started. + if (recordObservation.equals("Started Recovery")) { + if (recoveryStartRows.size() > recoveryEndRows.size()) { + response.put("detailedResponse", "Animal " + recordId + " still has open anesthesia recovery records."); + response.put("success", false); + return response; + } + } + } + catch (Exception e) { + response.put("detailedResponse", "There was an issue querying the necessary datasets for anesthesia recovery validation: " + e.getMessage()); + response.put("success", false); + return response; + } + // Returns successfully. + _log.info("Successfully updated the anesthesia recovery dataset."); + response.put("detailedResponse", "Anesthesia table was successfully updated for animal: " + recordId); + response.put("success", true); + return response; + } + } -// public boolean isAliveAndAtCenter = false; -// -// private String animalid; -// -// public String getAnimalid() {return animalid;} -// -// public void setAnimalid(String animalid) {this.animalid = animalid;} -// -// public void setIsAliveAndAtCenter(boolean checkAlive) {this.isAliveAndAtCenter = checkAlive;} - } } From bf26df232de67c339307df9e1fd33d08bc5153b0 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Fri, 10 Oct 2025 17:05:07 -0500 Subject: [PATCH 03/12] Updated permission for wnprc_ehrController anesthesia upload api action. --- .../labkey/wnprc_ehr/WNPRC_EHRController.java | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index 59ff8b8ae..3c5accc5a 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -2458,66 +2458,8 @@ public Object execute(NecropsyEditRequestNotificationForm form, BindException er } @RequiresLogin -// @RequiresNoPermission public static class UpdateAnesthesiaRecoveryDatasetAction extends MutatingApiAction { -// @Override -// public void validateForm(SimpleApiJsonForm form, Errors errors) { -// -// // Verifies passed-in arguments are not null. -// if (form.getJsonObject() == null) { -// errors.reject(ERROR_MSG, "JSON argument cannot be null."); -// return; -// } -// JSONObject myForm = form.getJsonObject(); -// _log.info("TEST MESSAGE: JSON argument is: " + myForm); -// -// // Retrieves the passed-in arguments. -// String recordId = myForm.get("Id").toString(); -// String recordRoom = myForm.get("room").toString(); -// String recordDate = myForm.get("date").toString(); -// String recordObservation = myForm.get("observation").toString(); -// String recordRecoveryStart = myForm.get("recoveryStart").toString(); -// String recordObserverComments = myForm.get("observerComments").toString(); -// String recordObserver = myForm.get("observer").toString(); -// String recordRecoveryId = myForm.get("recoveryId").toString(); -// -// _log.info("TEST MESSAGE: ANESTHESIA VALIDATION 1"); -// -// _log.info("TEST MESSAGE 3:" + -// "ID: " + recordId + -// "ROOM: " + recordRoom + -// "DATE: " + recordDate + -// "OBSERVATION: " + recordObservation + -// "RECOVERY START: " + recordRecoveryStart + -// "OBSERVER COMMENTS: " + recordObserverComments + -// "OBSERVER: " + recordObserver + -// "RECOVERY ID: " + recordRecoveryId -// ); -// -// AnimalVerifier avrh1234 = new AnimalVerifier("rh1234", getUser(), getContainer()); // Does not exist -// AnimalVerifier avc19006 = new AnimalVerifier("c19006", getUser(), getContainer()); // Dead -// AnimalVerifier avc19007 = new AnimalVerifier("c19007", getUser(), getContainer()); // Alive -// -// AnimalVerifier av = new AnimalVerifier("rh1234", getUser(), getContainer()); // Alive -// -// -// // TODO: Validate data here. -// -// -// -// -//// public boolean isAliveAndAtCenter = false; -//// -//// private String animalid; -//// -//// public String getAnimalid() {return animalid;} -//// -//// public void setAnimalid(String animalid) {this.animalid = animalid;} -//// -//// public void setIsAliveAndAtCenter(boolean checkAlive) {this.isAliveAndAtCenter = checkAlive;} -// } - @Override public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception { // Creates function variables. From 76558248d877aa7967eaa2bfad8fc354f2568457 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Fri, 31 Oct 2025 16:52:54 -0500 Subject: [PATCH 04/12] Added line to validate animal is at the center. --- .../src/org/labkey/wnprc_ehr/WNPRC_EHRController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index 3c5accc5a..f4b065a6f 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -2486,7 +2486,12 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep SimpleFilter demographicsFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); String[] demographicsTargetColumns = new String[]{"Id", "calculated_status"}; ArrayList> demographicsRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", demographicsFilter, null, demographicsTargetColumns); - if (!demographicsRows.isEmpty()) { + if (demographicsRows.isEmpty()) { + response.put("detailedResponse", "Animal " + recordId + " does not currently exist at the center."); + response.put("success", false); + return response; + } + else { if (!demographicsRows.get(0).get("calculated_status").equals("Alive")) { response.put("detailedResponse", "Animal " + recordId + " is not currently alive at the center."); response.put("success", false); From dad0ff24d57b13de271032e07822070a2fb08dd9 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Fri, 24 Apr 2026 17:09:48 -0500 Subject: [PATCH 05/12] -Updated the anesthesiaRecovery validation to include a time-check which verifies the server time is within 10 minutes of the iPad time. -Also updated the validation to correctly verify there are no animals with active recoveries before starting a new one. --- .../labkey/wnprc_ehr/WNPRC_EHRController.java | 96 +++++++++++++++---- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index f4b065a6f..004eabe32 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable; import org.joda.time.DateTime; import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; import org.json.JSONArray; import org.json.JSONObject; import org.jsoup.Jsoup; @@ -133,7 +134,9 @@ import java.sql.Timestamp; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; @@ -144,6 +147,8 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; + import org.labkey.api.action.SimpleApiJsonForm; import org.springframework.validation.Errors; @@ -2470,7 +2475,9 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep // Verifies passed-in arguments are not null. if (form.getJsonObject() == null) { - response.put("detailedResponse", "JSON argument cannot be null."); + String issueDetails = "JSON argument cannot be null."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); response.put("success", false); return response; } @@ -2479,6 +2486,39 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep JSONObject myForm = form.getJsonObject(); String recordId = myForm.get("Id").toString(); String recordObservation = myForm.get("observation").toString(); + String deviceDate = myForm.get("date").toString(); + String timezoneOffset = myForm.get("timezoneOffset").toString(); + String timezone = myForm.get("timezone").toString(); + + // Gets the current server time & offset. + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + java.time.LocalDateTime serverDate = java.time.LocalDateTime.now(); // Explicitly import Java here, otherwise script defaults to joda time due to both being imported above. + java.time.LocalDateTime serverDatePlus10 = serverDate.plusMinutes(10); + java.time.LocalDateTime serverDateMinus10 = serverDate.minusMinutes(10); + String formattedServerDate = serverDate.format(formatter); + ZoneId currentTimezone = ZoneId.systemDefault(); + ZoneOffset currentOffset = OffsetDateTime.now().getOffset(); + + // Verifies the iOS clock matches our server timezone. + java.time.LocalDateTime deviceDateAsLocalDateTime = java.time.LocalDateTime.parse(deviceDate, formatter); + if (deviceDateAsLocalDateTime.isBefore(serverDateMinus10) || deviceDateAsLocalDateTime.isAfter(serverDatePlus10)) { + String issueDetails = "Your current device time is over 10 minutes off from the current server time. Please update your current device time."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); + response.put("success", false); + return response; + } + + // Logs debug data for setting up timezone validation in the future. + _log.info( + "Anesthesia Recovery Time Test" + System.lineSeparator() + + "iOS Parsed Date: [" + deviceDate + "]" + System.lineSeparator() + + "iOS Timezone: [" + timezone + "]" + System.lineSeparator() + + "iOS Offset: [" + timezoneOffset + "]" + System.lineSeparator() + + "Java Parsed Date: [" + formattedServerDate + "]" + System.lineSeparator() + + "Server Timezone: [" + currentTimezone + "]" + System.lineSeparator() + + "Server Offset: [" + currentOffset + "]" + System.lineSeparator() + ); // Retrieves all necessary data. try { @@ -2487,38 +2527,60 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep String[] demographicsTargetColumns = new String[]{"Id", "calculated_status"}; ArrayList> demographicsRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", demographicsFilter, null, demographicsTargetColumns); if (demographicsRows.isEmpty()) { - response.put("detailedResponse", "Animal " + recordId + " does not currently exist at the center."); + String issueDetails = "Animal " + recordId + " does not currently exist at the center."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); response.put("success", false); return response; } else { if (!demographicsRows.get(0).get("calculated_status").equals("Alive")) { - response.put("detailedResponse", "Animal " + recordId + " is not currently alive at the center."); + String issueDetails = "Animal " + recordId + " is not currently alive at the center."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); response.put("success", false); return response; } } - // Gets all recoveries started. - SimpleFilter recoveryStartFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); - recoveryStartFilter.addCondition("observation", "Started Recovery", CompareType.EQUAL); - String[] recoveryStartTargetColumn = new String[]{"Id"}; - ArrayList> recoveryStartRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryStartFilter, null, recoveryStartTargetColumn); - // Gets all recoveries finished. - SimpleFilter recoveryEndFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); - recoveryEndFilter.addCondition("observation", "Fully Recovered", CompareType.EQUAL); - String[] recoveryEndTargetColumn = new String[]{"Id"}; - ArrayList> recoveryEndRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryEndFilter, null, recoveryEndTargetColumn); - // Verifies all recoveries have been closed before a new recovery can be started. - if (recordObservation.equals("Started Recovery")) { - if (recoveryStartRows.size() > recoveryEndRows.size()) { - response.put("detailedResponse", "Animal " + recordId + " still has open anesthesia recovery records."); + // Verifies there are no active recoveries ONLY if this an import. + if (recordObservation.equals("Imported")) { + // Gets all recoveries started. + SimpleFilter recoveryStartFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); + recoveryStartFilter.addCondition("observation", "Imported", CompareType.EQUAL); + String[] recoveryStartTargetColumn = new String[]{"recoveryId"}; + ArrayList> recoveryStartRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryStartFilter, null, recoveryStartTargetColumn); + // Gets all recoveries finished. + SimpleFilter recoveryEndFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); + recoveryEndFilter.addCondition("observation", "Fully Recovered", CompareType.EQUAL); + String[] recoveryEndTargetColumn = new String[]{"recoveryId"}; + ArrayList> recoveryEndRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryEndFilter, null, recoveryEndTargetColumn); + // Converts all finished recoveries into a set for fast comparison. + Set finishedIds = recoveryEndRows.stream() + .map(row -> row.get("recoveryId")) + .filter(id -> id != null) + .collect(Collectors.toSet()); + // Verifies every recovery ID 'started' has also 'ended'. + boolean allClosed = true; + ArrayList missingEndIds = new ArrayList<>(); + for (HashMap startRow : recoveryStartRows) { + String startId = startRow.get("recoveryId"); + if (!finishedIds.contains(startId)) { + allClosed = false; + missingEndIds.add(startId); + } + } + if (!allClosed) { + String issueDetails = "The following recoveries are still open: " + missingEndIds; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); response.put("success", false); return response; } } } catch (Exception e) { + _log.info("Error updating the anesthesia recovery dataset: " + e.getMessage()); response.put("detailedResponse", "There was an issue querying the necessary datasets for anesthesia recovery validation: " + e.getMessage()); response.put("success", false); return response; From 666f92ead17c49a6d304ef90609dd0b3a920ae6b Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Mon, 8 Jun 2026 14:49:28 -0500 Subject: [PATCH 06/12] anesthesiaRecovery.query.xml: Created XML for anesthesia recovery dataset. AnesthesiaRecoveryReviewNotification.java: Created email notification. NotificationToolkit.java: Updated create query URL function so it works for multiple clauses. WNPRC_EHRModule.java: Added new notification here. WNPRC_EHRController.java: Updated the trigger validation function so it now handles the insert as well as validation. --- .../study/anesthesiaRecovery.query.xml | 19 ++ .../labkey/wnprc_ehr/WNPRC_EHRController.java | 239 ++++++++++++++++-- .../org/labkey/wnprc_ehr/WNPRC_EHRModule.java | 1 + .../AnesthesiaRecoveryReviewNotification.java | 149 +++++++++++ .../notification/NotificationToolkit.java | 26 +- 5 files changed, 406 insertions(+), 28 deletions(-) create mode 100644 WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml create mode 100644 WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml new file mode 100644 index 000000000..718868264 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml @@ -0,0 +1,19 @@ + + + + + + + Task Id + + ehr + tasks + taskid + + /ehr/WNPRC/EHR/taskDetails.view?formtype=Anesthesia%20Recovery&taskid=${taskid} + + +
+
+
+
\ No newline at end of file diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index 004eabe32..59df5d9e6 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -59,6 +59,7 @@ import org.labkey.api.module.Module; import org.labkey.api.module.ModuleLoader; import org.labkey.api.module.ModuleProperty; +import org.labkey.api.qc.QCStateManager; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.FieldKey; import org.labkey.api.query.QueryHelper; @@ -2467,13 +2468,14 @@ public static class UpdateAnesthesiaRecoveryDatasetAction extends MutatingApiAct @Override public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception { - // Creates function variables. + // 1. Creates function variables. BatchValidationException batchErrors = new BatchValidationException(); ApiSimpleResponse response = new ApiSimpleResponse(); NotificationToolkit notificationToolkit = new NotificationToolkit(); _log.info("Started update to the anesthesia recovery dataset."); + int recoveryTaskId = 0; - // Verifies passed-in arguments are not null. + // 2. Verifies passed-in obeject is not null. if (form.getJsonObject() == null) { String issueDetails = "JSON argument cannot be null."; _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); @@ -2482,15 +2484,46 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep return response; } - // Retrieves passed-in arguments. + // 3. Retrieves passed-in arguments and verifies they all exist. + // REQUIRED (MANUALLY DEFINED) JSONObject myForm = form.getJsonObject(); String recordId = myForm.get("Id").toString(); + String recordRoom = myForm.get("room").toString(); String recordObservation = myForm.get("observation").toString(); String deviceDate = myForm.get("date").toString(); + String recoveryId = myForm.get("recoveryId").toString(); + String recordSubmitterInitials = myForm.get("submitterInitials").toString(); + String recordLocation = myForm.get("location").toString(); + String recordCage = myForm.get("cage").toString(); + // REQUIRED (CALCULATED) String timezoneOffset = myForm.get("timezoneOffset").toString(); String timezone = myForm.get("timezone").toString(); + String recordObserver = myForm.get("observer").toString(); + String recordAssignedTo = myForm.get("assignedTo").toString(); + String recordDeviceId = myForm.get("deviceId").toString(); + // OPTIONAL + String recordRecoveryStart = myForm.optString("recoveryStart"); + String recordObserverComments = myForm.optString("observerComments"); + String recordRecoverySpeed = myForm.optString("recoverySpeed"); + String recordRecoveryCondition = myForm.optString("recoveryCondition"); + String recordRecoveryReason = myForm.optString("recoveryReason"); + String recordGroupId = myForm.optString("groupId"); + String recordFinalizeComments = myForm.optString("finalizeComments"); + String recordCageLockSecure = myForm.optString("cageLockSecure"); + // TODO: Verify the necessary fields are filled in (condition, speed, etc) when doing finalize. Also verify required notes are added for rough/prolonged? recovery. + if (recordRecoverySpeed.equals("Prolonged") || recordRecoveryCondition.equals("Rough")) { + if (recordFinalizeComments.equals("")) { + String issueDetails = "You must include notes when marking a recovery as 'Prolonged' or 'Rough'"; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); + response.put("success", false); + return response; + } + } + + - // Gets the current server time & offset. + // 4. Gets the current server time & offset. DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); java.time.LocalDateTime serverDate = java.time.LocalDateTime.now(); // Explicitly import Java here, otherwise script defaults to joda time due to both being imported above. java.time.LocalDateTime serverDatePlus10 = serverDate.plusMinutes(10); @@ -2499,7 +2532,7 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep ZoneId currentTimezone = ZoneId.systemDefault(); ZoneOffset currentOffset = OffsetDateTime.now().getOffset(); - // Verifies the iOS clock matches our server timezone. + // 5. Verifies the iOS clock matches our server timezone. java.time.LocalDateTime deviceDateAsLocalDateTime = java.time.LocalDateTime.parse(deviceDate, formatter); if (deviceDateAsLocalDateTime.isBefore(serverDateMinus10) || deviceDateAsLocalDateTime.isAfter(serverDatePlus10)) { String issueDetails = "Your current device time is over 10 minutes off from the current server time. Please update your current device time."; @@ -2509,7 +2542,7 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep return response; } - // Logs debug data for setting up timezone validation in the future. + // 6. Logs debug data for setting up timezone validation in the future. _log.info( "Anesthesia Recovery Time Test" + System.lineSeparator() + "iOS Parsed Date: [" + deviceDate + "]" + System.lineSeparator() + @@ -2521,20 +2554,27 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep ); // Retrieves all necessary data. - try { - // Gets animal demographics record. + try + { + // TODO: Get location (and other animal info) below instead of passing-in. + // 7. Gets animal demographics record. SimpleFilter demographicsFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); String[] demographicsTargetColumns = new String[]{"Id", "calculated_status"}; ArrayList> demographicsRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", demographicsFilter, null, demographicsTargetColumns); - if (demographicsRows.isEmpty()) { + if (demographicsRows.isEmpty()) + { + // Fails if animal does not exist at the center. String issueDetails = "Animal " + recordId + " does not currently exist at the center."; _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); response.put("detailedResponse", issueDetails); response.put("success", false); return response; } - else { - if (!demographicsRows.get(0).get("calculated_status").equals("Alive")) { + else + { + // Fails if animal is not currently alive at the center. + if (!demographicsRows.get(0).get("calculated_status").equals("Alive")) + { String issueDetails = "Animal " + recordId + " is not currently alive at the center."; _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); response.put("detailedResponse", issueDetails); @@ -2543,8 +2583,9 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep } } - // Verifies there are no active recoveries ONLY if this an import. - if (recordObservation.equals("Imported")) { + // 8. Verifies there are no active recoveries ONLY if this an import. + if (recordObservation.equals("Imported")) + { // Gets all recoveries started. SimpleFilter recoveryStartFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); recoveryStartFilter.addCondition("observation", "Imported", CompareType.EQUAL); @@ -2563,14 +2604,17 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep // Verifies every recovery ID 'started' has also 'ended'. boolean allClosed = true; ArrayList missingEndIds = new ArrayList<>(); - for (HashMap startRow : recoveryStartRows) { + for (HashMap startRow : recoveryStartRows) + { String startId = startRow.get("recoveryId"); - if (!finishedIds.contains(startId)) { + if (!finishedIds.contains(startId)) + { allClosed = false; missingEndIds.add(startId); } } - if (!allClosed) { + if (!allClosed) + { String issueDetails = "The following recoveries are still open: " + missingEndIds; _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); response.put("detailedResponse", issueDetails); @@ -2578,8 +2622,167 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep return response; } } - } - catch (Exception e) { + + // 9. Creates or updates the taskID dataset. + if (recordObservation.equals("Imported")) { + // Creates a new Task corresponding to this recovery (using our passed-in recoveryID to create the task id). + Map taskRecord = new HashMap<>(); + String newTaskId = recoveryId; + taskRecord.put("taskid", newTaskId); + taskRecord.put("title", "Anesthesia Recovery"); + taskRecord.put("category", "task"); + taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); + taskRecord.put("formType", "Anesthesia Recovery"); + taskRecord.put("assignedTo", getUser().getUserId()); + // Inserts the task into the 'tasks' dataset. + List> taskToInsert = null; + taskToInsert = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); + TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); + QueryUpdateService service = ti.getUpdateService(); + BatchValidationException validationTaskException = new BatchValidationException(); + List> insertedTask = service.insertRows(getUser(), getContainer(), taskToInsert, validationTaskException, null, null); + // Verifies task was inserted correctly. + if (taskToInsert.size() != insertedTask.size()) { + String issueDetails = "There was an issue creating a corresponding task for this recovery."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); + response.put("success", false); + return response; + } + recoveryTaskId = (int) Double.parseDouble(String.valueOf(insertedTask.get(0).get("rowid"))); + } + else if (recordObservation.equals("Laying Down") || recordObservation.equals("Sitting Upright")) { + // Updates the Task corresponding to this recovery. + Map taskRecord = new HashMap<>(); + String existingTaskId = recoveryId; + taskRecord.put("taskid", existingTaskId); + taskRecord.put("title", "Anesthesia Recovery"); + taskRecord.put("category", "task"); + taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); + taskRecord.put("formType", "Anesthesia Recovery"); + taskRecord.put("assignedTo", getUser().getUserId()); + // Updates the task in the 'tasks' dataset. + List> taskToUpdate = null; + taskToUpdate = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); + TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); + QueryUpdateService service = ti.getUpdateService(); + BatchValidationException validationTaskException = new BatchValidationException(); + List> updatedTask = service.updateRows(getUser(), getContainer(), taskToUpdate, taskToUpdate, validationTaskException, null, null); + // Verifies task was updated correctly. + if (taskToUpdate.size() != updatedTask.size()) { + String issueDetails = "There was an issue completing the corresponding task for this recovery."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); + response.put("success", false); + return response; + } + recoveryTaskId = (int) Double.parseDouble(String.valueOf(updatedTask.get(0).get("rowid"))); + _log.info("RECOVERY TASK ID EXISTING A: " + existingTaskId); + _log.info("RECOVERY TASK ID EXISTING B: " + taskToUpdate); + _log.info("RECOVERY TASK ID EXISTING C: " + updatedTask); + } + else if (recordObservation.equals("Fully Recovered")) { + // Updates the Task corresponding to this recovery. + Map taskRecord = new HashMap<>(); + String existingTaskId = recoveryId; + taskRecord.put("taskid", existingTaskId); + taskRecord.put("title", "Anesthesia Recovery"); + taskRecord.put("category", "task"); + taskRecord.put("qcstate", EHRService.QCSTATES.Completed.getQCState(getContainer()).getRowId()); + taskRecord.put("formType", "Anesthesia Recovery"); + taskRecord.put("assignedTo", getUser().getUserId()); + // Updates the task in the 'tasks' dataset. + List> taskToUpdate = null; + taskToUpdate = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); + TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); + QueryUpdateService service = ti.getUpdateService(); + BatchValidationException validationTaskException = new BatchValidationException(); + List> updatedTask = service.updateRows(getUser(), getContainer(), taskToUpdate, taskToUpdate, validationTaskException, null, null); + // Verifies task was updated correctly. + if (taskToUpdate.size() != updatedTask.size()) { + String issueDetails = "There was an issue completing the corresponding task for this recovery."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); + response.put("success", false); + return response; + } + recoveryTaskId = (int) Double.parseDouble(String.valueOf(updatedTask.get(0).get("rowid"))); + } + else if (recordObservation.equals("Deleted")) { + // Creates a new Task corresponding to this recovery (using our passed-in recoveryID). + Map taskRecord = new HashMap<>(); + String existingTaskId = recoveryId; + taskRecord.put("taskid", existingTaskId); + taskRecord.put("title", "Anesthesia Recovery"); + taskRecord.put("category", "task"); + taskRecord.put("qcstate", EHRService.QCSTATES.DeleteRequested.getQCState(getContainer()).getRowId()); + + // TEST +// QCStateManager.getInstance().getStates(ctx.getContainer()) + + taskRecord.put("qcstate", QCStateManager.getInstance().getStates(getContainer())); + + // TEST + + taskRecord.put("formType", "Anesthesia Recovery"); + taskRecord.put("assignedTo", getUser().getUserId()); + // Updates the task in the 'tasks' dataset. + List> taskToUpdate = null; + taskToUpdate = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); + TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); + QueryUpdateService service = ti.getUpdateService(); + BatchValidationException validationTaskException = new BatchValidationException(); + List> updatedTask = service.updateRows(getUser(), getContainer(), taskToUpdate, taskToUpdate, validationTaskException, null, null); + // Verifies task was updated correctly. + if (taskToUpdate.size() != updatedTask.size()) { + String issueDetails = "There was an issue deleting the corresponding task for this recovery."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); + response.put("success", false); + return response; + } + recoveryTaskId = (int) Double.parseDouble(String.valueOf(updatedTask.get(0).get("rowid"))); + } + + // 10. Updates the anesthesiaRecovery dataset. + // Creates a new Task corresponding to this recovery (using our passed-in recoveryID to create the task id). + Map anesthesiaEntry = new HashMap<>(); + anesthesiaEntry.put("Id", recordId); + anesthesiaEntry.put("room", recordRoom); + anesthesiaEntry.put("date", deviceDate); + anesthesiaEntry.put("observation", recordObservation); + anesthesiaEntry.put("recoveryStart", recordRecoveryStart); + anesthesiaEntry.put("observerComments", recordObserverComments); + anesthesiaEntry.put("observer", recordObserver); + anesthesiaEntry.put("recoveryId", recoveryId); + anesthesiaEntry.put("recoverySpeed", recordRecoverySpeed); + anesthesiaEntry.put("recoveryCondition", recordRecoveryCondition); + anesthesiaEntry.put("assignedTo", recordAssignedTo); + anesthesiaEntry.put("recoveryReason", recordRecoveryReason); + anesthesiaEntry.put("submitterInitials", recordSubmitterInitials); + anesthesiaEntry.put("groupId", recordGroupId); + anesthesiaEntry.put("finalizeComments", recordFinalizeComments); + anesthesiaEntry.put("location", recordLocation); + anesthesiaEntry.put("cage", recordCage); + anesthesiaEntry.put("cageLockSecure", recordCageLockSecure); + anesthesiaEntry.put("deviceId", recordDeviceId); + anesthesiaEntry.put("taskId", recoveryTaskId); + // Inserts the task into the 'anesthesiaRecovery' dataset. + List> observationToInsert = null; + observationToInsert = SimpleQueryUpdater.makeRowsCaseInsensitive(anesthesiaEntry); + TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "study").getTable("anesthesiaRecovery"); + QueryUpdateService service = ti.getUpdateService(); + BatchValidationException validationTaskException = new BatchValidationException(); + List> insertedObservation = service.insertRows(getUser(), getContainer(), observationToInsert, validationTaskException, null, null); + // Verifies observation was inserted correctly. + if (observationToInsert.size() != insertedObservation.size()) { + String issueDetails = "There was an issue inserting the observation row into the anesthesia recovery dataset."; + _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); + response.put("detailedResponse", issueDetails); + response.put("success", false); + return response; + } + } catch (Exception e) { _log.info("Error updating the anesthesia recovery dataset: " + e.getMessage()); response.put("detailedResponse", "There was an issue querying the necessary datasets for anesthesia recovery validation: " + e.getMessage()); response.put("success", false); diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java index 85a6d4ade..821f19026 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java @@ -397,6 +397,7 @@ public void registerNotifications() { new ClinpathResultAlertsRevamp(this), new LargeInfantAlertsRevamp(this), new OverdueWeightAlertsRevamp(this), + new AnesthesiaRecoveryReviewNotification(this), new SiteErrorAlertsRevamp(this), new WeightAlertsRevamp(this) ); diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java new file mode 100644 index 000000000..f75381a3b --- /dev/null +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java @@ -0,0 +1,149 @@ +package org.labkey.wnprc_ehr.notification; + +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.module.Module; +import org.labkey.api.security.User; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class AnesthesiaRecoveryReviewNotification extends AbstractEHRNotification { + // Class Variables + NotificationToolkit notificationToolkit = new NotificationToolkit(); + NotificationToolkit.DateToolkit dateToolkit = new NotificationToolkit.DateToolkit(); + NotificationToolkit.StyleToolkit styleToolkit = new NotificationToolkit.StyleToolkit(); + + + + + + // Constructors + + /** + * This constructor is used to register the notification in WNPRC_EHRModule.java. + * + * @param owner + */ + public AnesthesiaRecoveryReviewNotification(Module owner) { super(owner); } + + + + + + // Notification Details + @Override + public String getName() { return "Anesthesia Recovery Review"; } + + @Override + public String getDescription() { + return "This report is designed to identify any issues with the Anesthesia Recoveries dataset."; + } + @Override + public String getEmailSubject(Container c) { + return "Anesthesia Recovery Review: " + dateToolkit.getCurrentTime(); + } + @Override + public String getScheduleDescription() { return "Daily at 4:00PM"; } + @Override + public String getCronString() { return notificationToolkit.createCronString("0", "16", "*"); } + @Override + public String getCategory() { return "iOS App Notifications"; } + + + + + + // Message Creation + public String getMessageBodyHTML(Container c, User u) { + // Creates variables & gets data. + final StringBuilder messageBody = new StringBuilder(); + AnesthesiaRecoveryReviewNotificationObject myRecoveriesObject = new AnesthesiaRecoveryReviewNotificationObject(c, u); + + // Creates CSS. + messageBody.append(styleToolkit.beginStyle()); + messageBody.append(styleToolkit.setBasicTableStyle()); + messageBody.append(styleToolkit.setHeaderRowBackgroundColor("#d9d9d9")); + messageBody.append(styleToolkit.endStyle()); + + // Begins message info. + messageBody.append("

This email contains all unclosed anesthesia recoveries. It was run on: " + dateToolkit.getCurrentTime() + "

"); + + // Creates table. + if (myRecoveriesObject.unclosedRecoveries.isEmpty()) { + notificationToolkit.sendEmptyNotificationRevamp(c, u, "Anesthesia Recovery Review"); + return null; +// messageBody.append("All anesthesia recoveries have been closed."); // TODO: Use this if users want emails to still send when all recoveries are closed. + } + else { + for (HashMap result : myRecoveriesObject.unclosedRecoveries) { + messageBody.append(result.get("Id") + "
"); + } + messageBody.append(notificationToolkit.createHyperlink("Click here to view recoveries


", myRecoveriesObject.unclosedRecoveriesURL)); + } + + // Returns message. + return messageBody.toString(); + } + + + public static class AnesthesiaRecoveryReviewNotificationObject { + Container c; + User u; + NotificationToolkit notificationToolkit = new NotificationToolkit(); + NotificationToolkit.DateToolkit dateToolkit = new NotificationToolkit.DateToolkit(); + + // Constructor function. + public AnesthesiaRecoveryReviewNotificationObject(Container currentContainer, User currentUser) { + this.c = currentContainer; + this.u = currentUser; + this.getUnclosedAnesthesiaRecoveries(); + } + + // Find all anesthesia recoveries that have been opened, but not closed. + ArrayList> unclosedRecoveries; + String unclosedRecoveriesURL; + private void getUnclosedAnesthesiaRecoveries() { + // Creates filter. + SimpleFilter openedFilter = new SimpleFilter("observation", "Imported", CompareType.EQUAL); + SimpleFilter closedFilter = new SimpleFilter("observation", "Fully Recovered", CompareType.EQUAL); + // Creates sort. + Sort mySort = new Sort("Id"); + // Creates columns to retrieve. + String[] targetColumns = new String[]{"Id", "recoveryId"}; // TODO: Change this to task after implementing TaskID (only needed if we remove recoveryId). + // Runs query. + ArrayList> openedArray = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(c, u, "study", "anesthesiaRecovery", openedFilter, mySort, targetColumns); + ArrayList> closedArray = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(c, u, "study", "anesthesiaRecovery", closedFilter, mySort, targetColumns); + + // 1. Extract recoveryIds from closedArray into a Set. + Set closedIds = closedArray.stream() + .map(map -> map.get("recoveryId")) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + // 2. Filter openedArray to find items NOT in the closedIds set. + List> unclosedArray = openedArray.stream() + .filter(map -> !closedIds.contains(map.get("recoveryId"))) + .toList(); + // 3. Creates a URL consisting of all unclosed ID's. CompareType.IN requries a semicolon separated string list. + List unclosedRecoveryIds = unclosedArray.stream() + .map(map -> map.get("recoveryId")) + .filter(Objects::nonNull) + .toList(); + String unclosedRecoverIdsAsString = String.join(";", unclosedRecoveryIds); + SimpleFilter unclosedFilter = new SimpleFilter("recoveryId", unclosedRecoverIdsAsString, CompareType.IN); + String viewQueryURL = notificationToolkit.createQueryURL(c, "execute", "study", "anesthesiaRecovery", unclosedFilter); + + // Returns data. + this.unclosedRecoveries = new ArrayList<>(unclosedArray); + this.unclosedRecoveriesURL = viewQueryURL; + } + } +} diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NotificationToolkit.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NotificationToolkit.java index 16bf1dec6..e935e34e8 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NotificationToolkit.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NotificationToolkit.java @@ -855,6 +855,7 @@ public ArrayList sortSetWithNulls(Set setToSort) { // ColonyInformationObject > getLivingAnimalsWithMultipleActiveHousingRecords // ColonyInformationObject > getAllRecordsWithPotentialHousingConditionProblems // ColonyInformationObject > getAllRecordsWithCalculatedStatusFieldProblems + // AnesthesiaRecoveryReviewNotificationObject > getUnclosedAnesthesiaRecoveries /** * Creates a URL for a query matching user arguments. * WARNING: This should only be used with a SimpleFilter that has clauses containing only one field key. You can use multiple clauses and multiple values for each, but each clause should only have one key. @@ -888,22 +889,27 @@ else if (executeOrUpdate.equals("update")) { // Gets clause key. FieldKey clauseKey = currentClause.getFieldKeys().get(0); //TODO: Add in comment that this should only be used with clauses containing one field key for each clause. - // Gets clause value. - StringBuilder clauseValue = new StringBuilder(); - if (currentClause.getParamVals() != null) { - for (Object paramValue : currentClause.getParamVals()) { - clauseValue.append(paramValue.toString()); -// clauseValue.append(";"); - } - } - - // Gets clause compare. + // Gets clause compare and value. CompareType clauseCompare = null; + StringBuilder clauseValue = new StringBuilder(); if (currentClause instanceof CompareType.CompareClause) { clauseCompare = ((CompareType.CompareClause)currentClause).getCompareType(); + // Gets clause value. + if (currentClause.getParamVals() != null) { + for (Object paramValue : currentClause.getParamVals()) { + clauseValue.append(paramValue.toString()); + } + } } else if (currentClause instanceof SimpleFilter.InClause) { clauseCompare = ((SimpleFilter.InClause) currentClause).getCompareType(); + // Gets clause value. + if (currentClause.getParamVals() != null) { + for (Object paramValue : currentClause.getParamVals()) { + clauseValue.append(paramValue.toString()); + clauseValue.append(";"); + } + } } else { return ""; From 14fe655a2de6cf1327210fa75d0335332236d876 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Mon, 22 Jun 2026 18:42:01 -0500 Subject: [PATCH 07/12] - AnesthesiaRecoveryReviewNotification.java: Created this new notification to show unfinished recoveries. - WNPRC_EHRCustomizer.java: Added customizer funciton to add start time column. - anesthesiaRecovery/... Created 3 new qviews for the anesthesia recovery table. --- .../study/anesthesiaRecovery/.qview.xml | 28 +++++++++++++++++++ .../anesthesiaRecovery/Full History.qview.xml | 28 +++++++++++++++++++ .../anesthesiaRecovery/Summary.qview.xml | 11 ++++++++ .../AnesthesiaRecoveryReviewNotification.java | 4 +-- .../wnprc_ehr/table/WNPRC_EHRCustomizer.java | 24 +++++++++++++++- 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml create mode 100644 WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Full History.qview.xml create mode 100644 WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml new file mode 100644 index 000000000..6042771a3 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Full History.qview.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Full History.qview.xml new file mode 100644 index 000000000..5552f73c2 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Full History.qview.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml new file mode 100644 index 000000000..c5a74aa61 --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java index f75381a3b..538bb31b0 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java @@ -52,9 +52,9 @@ public String getEmailSubject(Container c) { return "Anesthesia Recovery Review: " + dateToolkit.getCurrentTime(); } @Override - public String getScheduleDescription() { return "Daily at 4:00PM"; } + public String getScheduleDescription() { return "Daily at 3:00PM"; } @Override - public String getCronString() { return notificationToolkit.createCronString("0", "16", "*"); } + public String getCronString() { return notificationToolkit.createCronString("0", "15", "*"); } @Override public String getCategory() { return "iOS App Notifications"; } diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java index 3a8b2ab58..43f57b205 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java @@ -101,9 +101,11 @@ else if (table.getName().equalsIgnoreCase("breeding_encounters") && table.getSch } else if (matches(table, "wnprc", "animal_requests")) { customizeAnimalRequestsTable((AbstractTableInfo) table); } + else if (matches(table, "wnprc", "anesthesiaRecovery")) { + customizeAnesthesiaRecoveryTable((AbstractTableInfo) table); + } else if (table.getName().equalsIgnoreCase("waterOrders")) appendEnddateFuture((AbstractTableInfo) table, "enddate"); - } } @@ -315,6 +317,26 @@ private void customizeFeedingTable(AbstractTableInfo ti) } + private void customizeAnesthesiaRecoveryTable(AbstractTableInfo ti) { + // Defines new customized column and display name. + String recoveryStartTimeColumnName = "recoveryStartTime"; + String recoveryStartTimeDisplayName = "Recovery Start Time"; + String recoveryStartTimeDescription = "This column shows the calculated original start time for this specific recovery (the date/time the first observation was made)."; + // Gets the dataset name. + String tableName = ti.getSchema().getName() + "." + ti.getName(); + // Creates SQL script to define what to show in column. + SQLFragment sql = new SQLFragment("(SELECT MIN(sub.date) " + + "FROM " + tableName + " sub " + + "WHERE sub.observation != 'imported' " + + "AND sub.recoveryId = '" + ExprColumn.STR_TABLE_ALIAS + ".recoveryId')" + ); + // Compiles data and creates the new column to insert. + ExprColumn newCol = new ExprColumn(ti, recoveryStartTimeColumnName, sql, JdbcType.TIMESTAMP); + newCol.setLabel(recoveryStartTimeDisplayName); + newCol.setDescription(recoveryStartTimeDescription); + ti.addColumn(newCol); + } + private void customizeBirthTable(AbstractTableInfo ti) { var cond = ti.getMutableColumn("cond"); From 56eb37f37ac4739d12c3b4e0071a93621c9b2721 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Thu, 9 Jul 2026 11:08:32 -0500 Subject: [PATCH 08/12] - WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml: Updated to show red lines under required fields. - WNPRC_EHR/resources/queries/wnprc_ios_app/session_log.query.xml: Created new xml to dfine this table. - WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java: Completely revamped validation trigger function - WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java: Updated anesthesiaRecovery customizer and added session log customizer. --- .../study/anesthesiaRecovery.query.xml | 28 + .../wnprc_ios_app/session_log.query.xml | 10 + .../labkey/wnprc_ehr/WNPRC_EHRController.java | 492 +++++++----------- .../wnprc_ehr/table/WNPRC_EHRCustomizer.java | 45 +- 4 files changed, 272 insertions(+), 303 deletions(-) create mode 100644 WNPRC_EHR/resources/queries/wnprc_ios_app/session_log.query.xml diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml index 718868264..f6fd82fbf 100644 --- a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml @@ -3,6 +3,16 @@ + + true + false + + + + true + false + + Task Id @@ -11,7 +21,25 @@ taskid /ehr/WNPRC/EHR/taskDetails.view?formtype=Anesthesia%20Recovery&taskid=${taskid} + true + false + + + + true + false + + + + true + false + + + + true + false +
diff --git a/WNPRC_EHR/resources/queries/wnprc_ios_app/session_log.query.xml b/WNPRC_EHR/resources/queries/wnprc_ios_app/session_log.query.xml new file mode 100644 index 000000000..6262ff6c1 --- /dev/null +++ b/WNPRC_EHR/resources/queries/wnprc_ios_app/session_log.query.xml @@ -0,0 +1,10 @@ + + + + + + +
+
+
+
\ No newline at end of file diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index 59df5d9e6..92ce6397b 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -18,10 +18,12 @@ import au.com.bytecode.opencsv.CSVWriter; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.text.WordUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.util.ArrayUtil; import org.jetbrains.annotations.Nullable; import org.joda.time.DateTime; import org.joda.time.LocalDate; @@ -55,6 +57,7 @@ import org.labkey.api.ehr.EHRService; import org.labkey.api.ehr.demographics.AnimalRecord; import org.labkey.api.exp.property.Domain; +import org.labkey.api.formSchema.Field; import org.labkey.api.ldk.notification.Notification; import org.labkey.api.module.Module; import org.labkey.api.module.ModuleLoader; @@ -67,6 +70,7 @@ import org.labkey.api.query.QueryUpdateService; import org.labkey.api.query.QueryUpdateServiceException; import org.labkey.api.query.UserSchema; +import org.labkey.api.query.ValidationException; import org.labkey.api.reader.ExcelFactory; import org.labkey.api.resource.DirectoryResource; import org.labkey.api.resource.FileResource; @@ -2468,335 +2472,233 @@ public static class UpdateAnesthesiaRecoveryDatasetAction extends MutatingApiAct @Override public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception { - // 1. Creates function variables. - BatchValidationException batchErrors = new BatchValidationException(); - ApiSimpleResponse response = new ApiSimpleResponse(); - NotificationToolkit notificationToolkit = new NotificationToolkit(); - _log.info("Started update to the anesthesia recovery dataset."); - int recoveryTaskId = 0; - - // 2. Verifies passed-in obeject is not null. - if (form.getJsonObject() == null) { - String issueDetails = "JSON argument cannot be null."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; - } - - // 3. Retrieves passed-in arguments and verifies they all exist. - // REQUIRED (MANUALLY DEFINED) - JSONObject myForm = form.getJsonObject(); - String recordId = myForm.get("Id").toString(); - String recordRoom = myForm.get("room").toString(); - String recordObservation = myForm.get("observation").toString(); - String deviceDate = myForm.get("date").toString(); - String recoveryId = myForm.get("recoveryId").toString(); - String recordSubmitterInitials = myForm.get("submitterInitials").toString(); - String recordLocation = myForm.get("location").toString(); - String recordCage = myForm.get("cage").toString(); - // REQUIRED (CALCULATED) - String timezoneOffset = myForm.get("timezoneOffset").toString(); - String timezone = myForm.get("timezone").toString(); - String recordObserver = myForm.get("observer").toString(); - String recordAssignedTo = myForm.get("assignedTo").toString(); - String recordDeviceId = myForm.get("deviceId").toString(); - // OPTIONAL - String recordRecoveryStart = myForm.optString("recoveryStart"); - String recordObserverComments = myForm.optString("observerComments"); - String recordRecoverySpeed = myForm.optString("recoverySpeed"); - String recordRecoveryCondition = myForm.optString("recoveryCondition"); - String recordRecoveryReason = myForm.optString("recoveryReason"); - String recordGroupId = myForm.optString("groupId"); - String recordFinalizeComments = myForm.optString("finalizeComments"); - String recordCageLockSecure = myForm.optString("cageLockSecure"); - // TODO: Verify the necessary fields are filled in (condition, speed, etc) when doing finalize. Also verify required notes are added for rough/prolonged? recovery. - if (recordRecoverySpeed.equals("Prolonged") || recordRecoveryCondition.equals("Rough")) { - if (recordFinalizeComments.equals("")) { - String issueDetails = "You must include notes when marking a recovery as 'Prolonged' or 'Rough'"; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; - } - } - + _log.info("INSERT CALLED: UpdateAnesthesiaRecoveryDatasetAction()"); + // 1. Sets up environment variables. + NotificationToolkit notificationToolkit = new NotificationToolkit(); + JSONObject response = new JSONObject(); + response.put("success",false); + response.put("detailedResponse", ""); + response.put("rowsUpdated", 0); - // 4. Gets the current server time & offset. - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); - java.time.LocalDateTime serverDate = java.time.LocalDateTime.now(); // Explicitly import Java here, otherwise script defaults to joda time due to both being imported above. - java.time.LocalDateTime serverDatePlus10 = serverDate.plusMinutes(10); - java.time.LocalDateTime serverDateMinus10 = serverDate.minusMinutes(10); - String formattedServerDate = serverDate.format(formatter); - ZoneId currentTimezone = ZoneId.systemDefault(); - ZoneOffset currentOffset = OffsetDateTime.now().getOffset(); - // 5. Verifies the iOS clock matches our server timezone. - java.time.LocalDateTime deviceDateAsLocalDateTime = java.time.LocalDateTime.parse(deviceDate, formatter); - if (deviceDateAsLocalDateTime.isBefore(serverDateMinus10) || deviceDateAsLocalDateTime.isAfter(serverDatePlus10)) { - String issueDetails = "Your current device time is over 10 minutes off from the current server time. Please update your current device time."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); + // 2. Gets passed-in data. + // 2a. Verifies an object with data was passed in. + JSONObject inputJson = form.getJsonObject(); + if (inputJson == null) { + response.put("detailedResponse", "No JSON payload provided."); + return response; + } + // 2b. Verifies passed-in object has rows. + if (!inputJson.has("rows") || inputJson.getJSONArray("rows").isEmpty()) { + response.put("detailedResponse", "No rows provided for the update."); return response; } + // 2c. Converts the JSONArray to the row map our loop expects. + List> rowsToValidate = new ArrayList<>(); + JSONArray rowsArray = inputJson.getJSONArray("rows"); + for (int i = 0; i < rowsArray.length(); i++) { + rowsToValidate.add(rowsArray.getJSONObject(i).toMap()); + } - // 6. Logs debug data for setting up timezone validation in the future. - _log.info( - "Anesthesia Recovery Time Test" + System.lineSeparator() + - "iOS Parsed Date: [" + deviceDate + "]" + System.lineSeparator() + - "iOS Timezone: [" + timezone + "]" + System.lineSeparator() + - "iOS Offset: [" + timezoneOffset + "]" + System.lineSeparator() + - "Java Parsed Date: [" + formattedServerDate + "]" + System.lineSeparator() + - "Server Timezone: [" + currentTimezone + "]" + System.lineSeparator() + - "Server Offset: [" + currentOffset + "]" + System.lineSeparator() - ); - // Retrieves all necessary data. - try - { - // TODO: Get location (and other animal info) below instead of passing-in. - // 7. Gets animal demographics record. - SimpleFilter demographicsFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); - String[] demographicsTargetColumns = new String[]{"Id", "calculated_status"}; - ArrayList> demographicsRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", demographicsFilter, null, demographicsTargetColumns); - if (demographicsRows.isEmpty()) - { - // Fails if animal does not exist at the center. - String issueDetails = "Animal " + recordId + " does not currently exist at the center."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; + // 3. Creates the object that collects errors from all failing rows, or the successful rows and tasks to upload. + BatchValidationException batchErrors = new BatchValidationException(); + List> validatedRows = new ArrayList<>(); + List> tasksToInsert = new ArrayList<>(); + List> tasksToUpdate = new ArrayList<>(); + + + // 4. Loops through each row and validates them. + for (int i = 0; i < rowsToValidate.size(); i++) { + // Retrieves the current. + Map row = rowsToValidate.get(i); + // Retrieves required values. + String id = row.get("Id") != null ? row.get("Id").toString() : null; + String recoveryReason = row.get("recoveryReason") != null ? row.get("recoveryReason").toString() : "none"; + String observer = row.get("observer") != null ? row.get("observer").toString() : null; + String recoveryId = row.get("recoveryId") != null ? row.get("recoveryId").toString() : null; + String observation = row.get("observation") != null ? row.get("observation").toString() : null; + String submitterInitials = row.get("submitterInitials") != null ? row.get("submitterInitials").toString() : null; + java.time.LocalDateTime serverDate = java.time.LocalDateTime.now(); // Explicitly import Java here, otherwise script defaults to joda time due to both being imported above. + // TODO: Ask users if initials should be required for deleting a row. + if (submitterInitials == null) { + submitterInitials = "empty"; } - else - { - // Fails if animal is not currently alive at the center. - if (!demographicsRows.get(0).get("calculated_status").equals("Alive")) - { - String issueDetails = "Animal " + recordId + " is not currently alive at the center."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; + // Retrieves optional values. + String observerComments = row.get("observerComments") != null ? row.get("observerComments").toString() : null; + String room = row.get("room") != null ? row.get("room").toString() : null; + String assignedTo = row.get("assignedTo") != null ? row.get("assignedTo").toString() : null; + String recoverySpeed = row.get("recoverySpeed") != null ? row.get("recoverySpeed").toString() : null; + String recoveryCondition = row.get("recoveryCondition") != null ? row.get("recoveryCondition").toString() : null; + String groupId = row.get("groupId") != null ? row.get("groupId").toString() : null; + String finalizeComments = row.get("finalizeComments") != null ? row.get("finalizeComments").toString() : null; + String location = row.get("location") != null ? row.get("location").toString() : null; + String cage = row.get("cage") != null ? row.get("cage").toString() : null; + String cageLockSecure = row.get("cageLockSecure") != null ? row.get("cageLockSecure").toString() : null; + String deviceId = row.get("deviceId") != null ? row.get("deviceId").toString() : null; + + // Sets data to new row variable. + Map validatedRow = new HashMap<>(); + validatedRow.put("Id", id); + validatedRow.put("date", serverDate); + validatedRow.put("recoveryReason", recoveryReason); + validatedRow.put("observer", observer); + validatedRow.put("recoveryId", recoveryId); + validatedRow.put("observation", observation); + validatedRow.put("submitterInitials", submitterInitials); + validatedRow.put("observerComments", observerComments); + validatedRow.put("room", room); + validatedRow.put("assignedTo", assignedTo); + validatedRow.put("recoverySpeed", recoverySpeed); + validatedRow.put("recoveryCondition", recoveryCondition); + validatedRow.put("groupId", groupId); + validatedRow.put("finalizeComments", finalizeComments); + validatedRow.put("location", location); + validatedRow.put("cage", cage); + validatedRow.put("cageLockSecure", cageLockSecure); + validatedRow.put("deviceId", deviceId); + + // 4a. Check row for required fields (doesn't require initials for a row deletion). + String[] requiredFields = { + "Id", "recoveryReason", "observer", "recoveryId", "observation", "submitterInitials" + }; + boolean missingField = false; + for (String field : requiredFields) { + if (validatedRow.get(field) == null || validatedRow.get(field).toString().trim().isEmpty()) { + batchErrors.addRowError(new ValidationException("Row " + (i + 1) + " is missing required field: " + field)); + missingField = true; + break; // Stop checking this row, move to next. } } + if (missingField) { + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } - // 8. Verifies there are no active recoveries ONLY if this an import. - if (recordObservation.equals("Imported")) - { - // Gets all recoveries started. - SimpleFilter recoveryStartFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); - recoveryStartFilter.addCondition("observation", "Imported", CompareType.EQUAL); - String[] recoveryStartTargetColumn = new String[]{"recoveryId"}; - ArrayList> recoveryStartRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryStartFilter, null, recoveryStartTargetColumn); - // Gets all recoveries finished. - SimpleFilter recoveryEndFilter = new SimpleFilter("id", recordId, CompareType.EQUAL); - recoveryEndFilter.addCondition("observation", "Fully Recovered", CompareType.EQUAL); - String[] recoveryEndTargetColumn = new String[]{"recoveryId"}; - ArrayList> recoveryEndRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveryEndFilter, null, recoveryEndTargetColumn); - // Converts all finished recoveries into a set for fast comparison. - Set finishedIds = recoveryEndRows.stream() - .map(row -> row.get("recoveryId")) - .filter(id -> id != null) - .collect(Collectors.toSet()); - // Verifies every recovery ID 'started' has also 'ended'. - boolean allClosed = true; - ArrayList missingEndIds = new ArrayList<>(); - for (HashMap startRow : recoveryStartRows) - { - String startId = startRow.get("recoveryId"); - if (!finishedIds.contains(startId)) - { - allClosed = false; - missingEndIds.add(startId); - } - } - if (!allClosed) - { - String issueDetails = "The following recoveries are still open: " + missingEndIds; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; + // 4b. Verifies animal is currently alive and exists at center. + SimpleFilter existsAliveAtCenterFilter = new SimpleFilter("id", id, CompareType.EQUAL); + String[] existsAliveAtCenterTargetColumns = new String[]{"Id", "calculated_status"}; + ArrayList> existsAliveAtCenterRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", existsAliveAtCenterFilter, null, existsAliveAtCenterTargetColumns); + if (existsAliveAtCenterRows == null || existsAliveAtCenterRows.isEmpty()) { + batchErrors.addRowError((new ValidationException("Animal does not currently exist at the center."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else if (!existsAliveAtCenterRows.get(0).get("calculated_status").equals("Alive")) { + batchErrors.addRowError((new ValidationException("Animal is no longer alive."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + + // 4c. Verifies no other active recoveries exist for the current animal (only if this is an 'import' observation). + if (observation.equals("Imported")) { + // Get started & finished recoveries. + String[] existingRecoveriesTargetColumn = new String[]{"recoveryId"}; + SimpleFilter recoveriesStartedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesStartedFilter.addCondition("observation", "Imported", CompareType.EQUAL); + SimpleFilter recoveriesFinishedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesFinishedFilter.addCondition("observation", "Fully Recovered;Deleted", CompareType.IN); + ArrayList> recoveriesStartedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesStartedFilter, null, existingRecoveriesTargetColumn); + ArrayList> recoveriesFinishedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesFinishedFilter, null, existingRecoveriesTargetColumn); + // Verify counts match. + if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows != null && !recoveriesFinishedRows.isEmpty() && recoveriesStartedRows.size() != recoveriesFinishedRows.size()) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before beginning a new one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. } } - // 9. Creates or updates the taskID dataset. - if (recordObservation.equals("Imported")) { - // Creates a new Task corresponding to this recovery (using our passed-in recoveryID to create the task id). - Map taskRecord = new HashMap<>(); - String newTaskId = recoveryId; - taskRecord.put("taskid", newTaskId); - taskRecord.put("title", "Anesthesia Recovery"); - taskRecord.put("category", "task"); + // 4d. Create or Update task into task dataset. + Map taskRecord = new HashMap<>(); + taskRecord.put("taskid", recoveryId); + taskRecord.put("title", "Anesthesia Recovery"); + taskRecord.put("category", "task"); + taskRecord.put("formType", "Anesthesia Recovery"); + taskRecord.put("assignedTo", getUser().getUserId()); + if (observation.equals("Imported")) { taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); - taskRecord.put("formType", "Anesthesia Recovery"); - taskRecord.put("assignedTo", getUser().getUserId()); - // Inserts the task into the 'tasks' dataset. - List> taskToInsert = null; - taskToInsert = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); - TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); - QueryUpdateService service = ti.getUpdateService(); - BatchValidationException validationTaskException = new BatchValidationException(); - List> insertedTask = service.insertRows(getUser(), getContainer(), taskToInsert, validationTaskException, null, null); - // Verifies task was inserted correctly. - if (taskToInsert.size() != insertedTask.size()) { - String issueDetails = "There was an issue creating a corresponding task for this recovery."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; - } - recoveryTaskId = (int) Double.parseDouble(String.valueOf(insertedTask.get(0).get("rowid"))); + tasksToInsert.add(taskRecord); } - else if (recordObservation.equals("Laying Down") || recordObservation.equals("Sitting Upright")) { - // Updates the Task corresponding to this recovery. - Map taskRecord = new HashMap<>(); - String existingTaskId = recoveryId; - taskRecord.put("taskid", existingTaskId); - taskRecord.put("title", "Anesthesia Recovery"); - taskRecord.put("category", "task"); + else if (observation.equals("Sitting Upright") || observation.equals("Laying Down")) { taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); - taskRecord.put("formType", "Anesthesia Recovery"); - taskRecord.put("assignedTo", getUser().getUserId()); - // Updates the task in the 'tasks' dataset. - List> taskToUpdate = null; - taskToUpdate = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); - TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); - QueryUpdateService service = ti.getUpdateService(); - BatchValidationException validationTaskException = new BatchValidationException(); - List> updatedTask = service.updateRows(getUser(), getContainer(), taskToUpdate, taskToUpdate, validationTaskException, null, null); - // Verifies task was updated correctly. - if (taskToUpdate.size() != updatedTask.size()) { - String issueDetails = "There was an issue completing the corresponding task for this recovery."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; - } - recoveryTaskId = (int) Double.parseDouble(String.valueOf(updatedTask.get(0).get("rowid"))); - _log.info("RECOVERY TASK ID EXISTING A: " + existingTaskId); - _log.info("RECOVERY TASK ID EXISTING B: " + taskToUpdate); - _log.info("RECOVERY TASK ID EXISTING C: " + updatedTask); + tasksToUpdate.add(taskRecord); } - else if (recordObservation.equals("Fully Recovered")) { - // Updates the Task corresponding to this recovery. - Map taskRecord = new HashMap<>(); - String existingTaskId = recoveryId; - taskRecord.put("taskid", existingTaskId); - taskRecord.put("title", "Anesthesia Recovery"); - taskRecord.put("category", "task"); + else if (observation.equals("Fully Recovered")) { taskRecord.put("qcstate", EHRService.QCSTATES.Completed.getQCState(getContainer()).getRowId()); - taskRecord.put("formType", "Anesthesia Recovery"); - taskRecord.put("assignedTo", getUser().getUserId()); - // Updates the task in the 'tasks' dataset. - List> taskToUpdate = null; - taskToUpdate = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); - TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); - QueryUpdateService service = ti.getUpdateService(); - BatchValidationException validationTaskException = new BatchValidationException(); - List> updatedTask = service.updateRows(getUser(), getContainer(), taskToUpdate, taskToUpdate, validationTaskException, null, null); - // Verifies task was updated correctly. - if (taskToUpdate.size() != updatedTask.size()) { - String issueDetails = "There was an issue completing the corresponding task for this recovery."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; - } - recoveryTaskId = (int) Double.parseDouble(String.valueOf(updatedTask.get(0).get("rowid"))); + tasksToUpdate.add(taskRecord); } - else if (recordObservation.equals("Deleted")) { - // Creates a new Task corresponding to this recovery (using our passed-in recoveryID). - Map taskRecord = new HashMap<>(); - String existingTaskId = recoveryId; - taskRecord.put("taskid", existingTaskId); - taskRecord.put("title", "Anesthesia Recovery"); - taskRecord.put("category", "task"); + else if (observation.equals("Deleted")) { taskRecord.put("qcstate", EHRService.QCSTATES.DeleteRequested.getQCState(getContainer()).getRowId()); + tasksToUpdate.add(taskRecord); + } - // TEST -// QCStateManager.getInstance().getStates(ctx.getContainer()) + // 4e. Current row is valid, added to validatedRows for the batch update. + validatedRows.add(validatedRow); - taskRecord.put("qcstate", QCStateManager.getInstance().getStates(getContainer())); + } - // TEST - taskRecord.put("formType", "Anesthesia Recovery"); - taskRecord.put("assignedTo", getUser().getUserId()); - // Updates the task in the 'tasks' dataset. - List> taskToUpdate = null; - taskToUpdate = SimpleQueryUpdater.makeRowsCaseInsensitive(taskRecord); - TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); - QueryUpdateService service = ti.getUpdateService(); - BatchValidationException validationTaskException = new BatchValidationException(); - List> updatedTask = service.updateRows(getUser(), getContainer(), taskToUpdate, taskToUpdate, validationTaskException, null, null); - // Verifies task was updated correctly. - if (taskToUpdate.size() != updatedTask.size()) { - String issueDetails = "There was an issue deleting the corresponding task for this recovery."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; - } - recoveryTaskId = (int) Double.parseDouble(String.valueOf(updatedTask.get(0).get("rowid"))); - } + // 6. Report all manual validation errors back to the user. + if (batchErrors.hasErrors()) { + String combinedErrors = batchErrors.getRowErrors().stream() + .map(error -> "Row " + error.getRowNumber() + ": " + error.getMessage()) + .collect(Collectors.joining("\n")); + response.put("success", false); + response.put("detailedResponse", combinedErrors); + response.put("errors", batchErrors.getRowErrors()); + return response; + } - // 10. Updates the anesthesiaRecovery dataset. - // Creates a new Task corresponding to this recovery (using our passed-in recoveryID to create the task id). - Map anesthesiaEntry = new HashMap<>(); - anesthesiaEntry.put("Id", recordId); - anesthesiaEntry.put("room", recordRoom); - anesthesiaEntry.put("date", deviceDate); - anesthesiaEntry.put("observation", recordObservation); - anesthesiaEntry.put("recoveryStart", recordRecoveryStart); - anesthesiaEntry.put("observerComments", recordObserverComments); - anesthesiaEntry.put("observer", recordObserver); - anesthesiaEntry.put("recoveryId", recoveryId); - anesthesiaEntry.put("recoverySpeed", recordRecoverySpeed); - anesthesiaEntry.put("recoveryCondition", recordRecoveryCondition); - anesthesiaEntry.put("assignedTo", recordAssignedTo); - anesthesiaEntry.put("recoveryReason", recordRecoveryReason); - anesthesiaEntry.put("submitterInitials", recordSubmitterInitials); - anesthesiaEntry.put("groupId", recordGroupId); - anesthesiaEntry.put("finalizeComments", recordFinalizeComments); - anesthesiaEntry.put("location", recordLocation); - anesthesiaEntry.put("cage", recordCage); - anesthesiaEntry.put("cageLockSecure", recordCageLockSecure); - anesthesiaEntry.put("deviceId", recordDeviceId); - anesthesiaEntry.put("taskId", recoveryTaskId); - // Inserts the task into the 'anesthesiaRecovery' dataset. - List> observationToInsert = null; - observationToInsert = SimpleQueryUpdater.makeRowsCaseInsensitive(anesthesiaEntry); - TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "study").getTable("anesthesiaRecovery"); - QueryUpdateService service = ti.getUpdateService(); - BatchValidationException validationTaskException = new BatchValidationException(); - List> insertedObservation = service.insertRows(getUser(), getContainer(), observationToInsert, validationTaskException, null, null); - // Verifies observation was inserted correctly. - if (observationToInsert.size() != insertedObservation.size()) { - String issueDetails = "There was an issue inserting the observation row into the anesthesia recovery dataset."; - _log.info("Error updating the anesthesia recovery dataset: " + issueDetails); - response.put("detailedResponse", issueDetails); - response.put("success", false); - return response; + // 7. Officially updates the database if all manual validation has been passed. + try (DbScope.Transaction transaction = StudySchema.getInstance().getSchema().getScope().ensureTransaction()) { + // 7a. Creates the environment variables. + TableInfo anesthesiaTableInfo = QueryService.get().getUserSchema(getUser(), getContainer(), "study").getTable("anesthesiaRecovery"); + TableInfo tasksTableInfo = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); + QueryUpdateService anesthesiaTableService = anesthesiaTableInfo.getUpdateService(); + QueryUpdateService tasksTableService = tasksTableInfo.getUpdateService(); + BatchValidationException dbErrors = new BatchValidationException(); + validatedRows = SimpleQueryUpdater.makeRowListCaseInsensitive(validatedRows); + tasksToInsert = SimpleQueryUpdater.makeRowListCaseInsensitive(tasksToInsert); + tasksToUpdate = SimpleQueryUpdater.makeRowListCaseInsensitive(tasksToUpdate); + + // 7b. Creates the tasks containing all rows to be inserted. + List> anesthesiaRowsToInsert = anesthesiaTableService.insertRows(getUser(), getContainer(), validatedRows, dbErrors, null, null); + tasksTableService.insertRows(getUser(), getContainer(), tasksToInsert, dbErrors, null, null); + tasksTableService.updateRows(getUser(), getContainer(), tasksToUpdate, tasksToUpdate, dbErrors, null, null); + + // 7c. Checks for errors in any row and aborts the insert + if (dbErrors.hasErrors()) { + throw dbErrors; } - } catch (Exception e) { - _log.info("Error updating the anesthesia recovery dataset: " + e.getMessage()); - response.put("detailedResponse", "There was an issue querying the necessary datasets for anesthesia recovery validation: " + e.getMessage()); + + // 7d. Executes the Task to insert all rows via a batch transaction. + transaction.commit(); + response.put("success", true);; + response.put("detailedResponse", "Database save successful."); + response.put("rowsUpdated", anesthesiaRowsToInsert.size()); + return response; + + } + // Catches any errors + catch (Exception e) { + _log.info("There was an issue inserting rows into the anesthesiaRecovery dataset: " + e.getMessage()); response.put("success", false); + response.put("detailedResponse", "Database save failed: " + e.getMessage()); return response; } + } + } - // Returns successfully. - _log.info("Successfully updated the anesthesia recovery dataset."); - response.put("detailedResponse", "Anesthesia table was successfully updated for animal: " + recordId); - response.put("success", true); - return response; + public static class AnesthesiaBatchForm { + private List> rows; + + public List> getRows() { + return rows; + } + + public void setRows(List> rows) { + this.rows = rows; } } + } diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java index 43f57b205..aa17b4895 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java @@ -101,9 +101,12 @@ else if (table.getName().equalsIgnoreCase("breeding_encounters") && table.getSch } else if (matches(table, "wnprc", "animal_requests")) { customizeAnimalRequestsTable((AbstractTableInfo) table); } - else if (matches(table, "wnprc", "anesthesiaRecovery")) { + else if (matches(table, "study", "anesthesiaRecovery")) { customizeAnesthesiaRecoveryTable((AbstractTableInfo) table); } + else if (matches(table, "wnprc_ios_app", "session_log")) { + customizeSessionLogTable((AbstractTableInfo) table); + } else if (table.getName().equalsIgnoreCase("waterOrders")) appendEnddateFuture((AbstractTableInfo) table, "enddate"); } @@ -322,14 +325,13 @@ private void customizeAnesthesiaRecoveryTable(AbstractTableInfo ti) { String recoveryStartTimeColumnName = "recoveryStartTime"; String recoveryStartTimeDisplayName = "Recovery Start Time"; String recoveryStartTimeDescription = "This column shows the calculated original start time for this specific recovery (the date/time the first observation was made)."; - // Gets the dataset name. - String tableName = ti.getSchema().getName() + "." + ti.getName(); // Creates SQL script to define what to show in column. - SQLFragment sql = new SQLFragment("(SELECT MIN(sub.date) " + - "FROM " + tableName + " sub " + - "WHERE sub.observation != 'imported' " + - "AND sub.recoveryId = '" + ExprColumn.STR_TABLE_ALIAS + ".recoveryId')" - ); + SQLFragment sql = new SQLFragment("(SELECT sub.date FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation != 'Imported' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId") + .append(" ORDER BY sub.created ASC LIMIT 1)"); // Compiles data and creates the new column to insert. ExprColumn newCol = new ExprColumn(ti, recoveryStartTimeColumnName, sql, JdbcType.TIMESTAMP); newCol.setLabel(recoveryStartTimeDisplayName); @@ -337,6 +339,33 @@ private void customizeAnesthesiaRecoveryTable(AbstractTableInfo ti) { ti.addColumn(newCol); } + private void customizeSessionLogTable(AbstractTableInfo ti) { + // 1. Defines new customized column and display name. + String numRecordsColumnName = "numRecords"; + String numRecordsDisplayName = "Number of Records"; + String numRecordsDescription = "This column shows the number of records returned from the current query."; + + // 2. Gets a reference to the wnprc schema's session log. + UserSchema wnprcSchema = getUserSchema(ti, "wnprc"); + if (wnprcSchema != null) { + TableInfo ogSessionLog = wnprcSchema.getTable("session_log"); + if (ogSessionLog != null) { + // 3. Creates SQL script to define what to show in our new column. + SQLFragment sql = new SQLFragment("(SELECT sub.number_of_records FROM ") + .append(ogSessionLog.getFromSQL("sub")) + .append(" WHERE sub.rowid = CAST(") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".original_row_id AS INTEGER) ORDER BY sub.created ASC LIMIT 1)"); + + // 4. Compiles and assigns our new column. + ExprColumn newCol = new ExprColumn(ti, numRecordsColumnName, sql, JdbcType.INTEGER); + newCol.setLabel(numRecordsDisplayName); + newCol.setDescription(numRecordsDescription); + ti.addColumn(newCol); + } + } + } + private void customizeBirthTable(AbstractTableInfo ti) { var cond = ti.getMutableColumn("cond"); From ddc35adb9ac39e8d3d96a6c95c21a5ddd64fcb79 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Mon, 3 Aug 2026 15:51:07 -0500 Subject: [PATCH 09/12] anesthesiaRecoveriesFullHistory.sql: Added data to show in 'full history' query. anesthesiaRecovery.query.xml: Added calculatd columns. anesthesiaRecovery/.qview.xml: Added calculatd columns. anesthesiaRecovery/Summary.qview.xml: Added total recovery time. wnprcFullHistory.sql: Added data to show in 'full history' section. wnprcFullHistory/.qview.xml: Added anesthesiaRecovery to options list. WNPRC_EHRController.java: Revamped API insert call to insert rows into anesthesia recovery with all necessary validation. AnesthesiaRecoveryReviewNotification.java: Updated email as needed. WNPRC_EHRCustomizer.java: Added calculations to retrieve recoveryReason, groupId, cage, location, and room from first 'imported' observation, then apply to all other observations so this doesn't need to be manually inserted on every observation after 'imported'. --- .../study/anesthesiaRecoveriesFullHistory.sql | 12 ++ .../study/anesthesiaRecovery.query.xml | 30 ++- .../study/anesthesiaRecovery/.qview.xml | 5 + .../anesthesiaRecovery/Summary.qview.xml | 1 + .../queries/study/wnprcFullHistory.sql | 21 ++- .../queries/study/wnprcFullHistory/.qview.xml | 2 +- .../labkey/wnprc_ehr/WNPRC_EHRController.java | 175 +++++++++++++----- .../AnesthesiaRecoveryReviewNotification.java | 71 ++++++- .../wnprc_ehr/table/WNPRC_EHRCustomizer.java | 146 ++++++++++++++- 9 files changed, 409 insertions(+), 54 deletions(-) create mode 100644 WNPRC_EHR/resources/queries/study/anesthesiaRecoveriesFullHistory.sql diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecoveriesFullHistory.sql b/WNPRC_EHR/resources/queries/study/anesthesiaRecoveriesFullHistory.sql new file mode 100644 index 000000000..8de749e9f --- /dev/null +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecoveriesFullHistory.sql @@ -0,0 +1,12 @@ +SELECT + Id, + date, + recoveryStartTime, + submitterInitials, + qcstate, + taskid, + observation, + recoverySpeed, + recoveryCondition, + totalRecoveryTime +FROM study.anesthesiaRecovery \ No newline at end of file diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml index f6fd82fbf..6777c912e 100644 --- a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml @@ -13,7 +13,11 @@ false - + + Recovery Start Time + + + Task Id ehr @@ -40,6 +44,30 @@ false + + Recovery Reason Final + + + + Group ID Final + + + + Cage Final + + + + Location Final + + + + Room Final + + + + Status + + diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml index 6042771a3..5a1dd4ac6 100644 --- a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml @@ -24,5 +24,10 @@ + + + + + diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml index c5a74aa61..7d6d023e9 100644 --- a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml @@ -4,6 +4,7 @@ + diff --git a/WNPRC_EHR/resources/queries/study/wnprcFullHistory.sql b/WNPRC_EHR/resources/queries/study/wnprcFullHistory.sql index 63c22ab93..0961bfb0b 100644 --- a/WNPRC_EHR/resources/queries/study/wnprcFullHistory.sql +++ b/WNPRC_EHR/resources/queries/study/wnprcFullHistory.sql @@ -38,4 +38,23 @@ SELECT null AS requestid FROM study.waterTotalByDate -WHERE TotalWater IS NOT NULL \ No newline at end of file +WHERE TotalWater IS NOT NULL + +UNION ALL +SELECT + Id AS Id, + date, + null AS project, + 'Anesthesia Recovery' AS dataset, + 'anesthesiarecovery' AS DataSetName, + 'Animal fully recovered.' AS remark, + 'Total Recovery Time: ' || CAST(CEILING(totalRecoveryTime) AS VARCHAR) || ' minutes' || CHR(10) || + 'Recovery Speed: ' || CAST(recoverySpeed AS VARCHAR) || CHR(10) || + 'Recovery Condition: ' || CAST(recoveryCondition AS VARCHAR) + AS description, + submitterInitials AS performedBy, + qcstate AS qcstate, + taskid AS taskid, + null AS requestid +FROM study.anesthesiaRecoveriesFullHistory +WHERE observation = 'Fully Recovered' \ No newline at end of file diff --git a/WNPRC_EHR/resources/queries/study/wnprcFullHistory/.qview.xml b/WNPRC_EHR/resources/queries/study/wnprcFullHistory/.qview.xml index 4c5f0b659..6e80b1c2f 100644 --- a/WNPRC_EHR/resources/queries/study/wnprcFullHistory/.qview.xml +++ b/WNPRC_EHR/resources/queries/study/wnprcFullHistory/.qview.xml @@ -16,7 +16,7 @@ - + \ No newline at end of file diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java index 92ce6397b..805815bc1 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java @@ -54,6 +54,7 @@ import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; import org.labkey.api.ehr.EHRDemographicsService; +import org.labkey.api.ehr.EHRQCState; import org.labkey.api.ehr.EHRService; import org.labkey.api.ehr.demographics.AnimalRecord; import org.labkey.api.exp.property.Domain; @@ -2472,14 +2473,28 @@ public static class UpdateAnesthesiaRecoveryDatasetAction extends MutatingApiAct @Override public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception { - _log.info("INSERT CALLED: UpdateAnesthesiaRecoveryDatasetAction()"); + _log.info("UPDATE CALLED: UpdateAnesthesiaRecoveryDatasetAction()"); - // 1. Sets up environment variables. + // 1. Sets up environment. + // 1a. Sets variables. NotificationToolkit notificationToolkit = new NotificationToolkit(); JSONObject response = new JSONObject(); response.put("success",false); response.put("detailedResponse", ""); response.put("rowsUpdated", 0); + // 1b. Gets QCState name. + var qcStateStarted = EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId(); + // TODO: Ask labkey why 'started' state isn't working. + // - Retrieved 'started' rowid with code below and it works. + // - Then I call this later: taskRecord.put("qcstate", qcStateStarted); + // - Which throws this error: Insufficient permissions to update: tasks to status: undefined, from: Scheduled + // - Why is this so much hassle trying to use the 'started' qc state and why can't I find any usage anywhere. +// String[] qcStateColumns = new String[]{"rowid"}; +// SimpleFilter qcStateFilter = new SimpleFilter("label", "Started", CompareType.EQUAL); +// ArrayList> startedQcStateResult = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "ehr", "status", qcStateFilter, null, qcStateColumns); +// if (startedQcStateResult != null || startedQcStateResult.isEmpty()) { +// qcStateStarted = Integer.parseInt(startedQcStateResult.get(0).get("rowid")); +// } // 2. Gets passed-in data. @@ -2504,44 +2519,56 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep // 3. Creates the object that collects errors from all failing rows, or the successful rows and tasks to upload. BatchValidationException batchErrors = new BatchValidationException(); - List> validatedRows = new ArrayList<>(); + List> rowsToInsert = new ArrayList<>(); + List> rowsToDelete = new ArrayList<>(); List> tasksToInsert = new ArrayList<>(); List> tasksToUpdate = new ArrayList<>(); // 4. Loops through each row and validates them. for (int i = 0; i < rowsToValidate.size(); i++) { - // Retrieves the current. + // Retrieves the current row. Map row = rowsToValidate.get(i); // Retrieves required values. String id = row.get("Id") != null ? row.get("Id").toString() : null; - String recoveryReason = row.get("recoveryReason") != null ? row.get("recoveryReason").toString() : "none"; String observer = row.get("observer") != null ? row.get("observer").toString() : null; String recoveryId = row.get("recoveryId") != null ? row.get("recoveryId").toString() : null; String observation = row.get("observation") != null ? row.get("observation").toString() : null; String submitterInitials = row.get("submitterInitials") != null ? row.get("submitterInitials").toString() : null; java.time.LocalDateTime serverDate = java.time.LocalDateTime.now(); // Explicitly import Java here, otherwise script defaults to joda time due to both being imported above. // TODO: Ask users if initials should be required for deleting a row. - if (submitterInitials == null) { + if (submitterInitials == null || submitterInitials == "") { submitterInitials = "empty"; } - // Retrieves optional values. + // Retrieves optional values (for all observations). String observerComments = row.get("observerComments") != null ? row.get("observerComments").toString() : null; - String room = row.get("room") != null ? row.get("room").toString() : null; String assignedTo = row.get("assignedTo") != null ? row.get("assignedTo").toString() : null; String recoverySpeed = row.get("recoverySpeed") != null ? row.get("recoverySpeed").toString() : null; String recoveryCondition = row.get("recoveryCondition") != null ? row.get("recoveryCondition").toString() : null; - String groupId = row.get("groupId") != null ? row.get("groupId").toString() : null; String finalizeComments = row.get("finalizeComments") != null ? row.get("finalizeComments").toString() : null; - String location = row.get("location") != null ? row.get("location").toString() : null; - String cage = row.get("cage") != null ? row.get("cage").toString() : null; String cageLockSecure = row.get("cageLockSecure") != null ? row.get("cageLockSecure").toString() : null; String deviceId = row.get("deviceId") != null ? row.get("deviceId").toString() : null; - + // Retrieves optional values (for fields that should only be updated on 'Imported' observations - customizer sets all other observation rows to reference 'Imported' row for these values). + String recoveryReason = null; + String groupId = null; + String cage = null; + String location = null; + String room = null; + if (observation.equals("Imported")) { + recoveryReason = row.get("recoveryReason") != null ? row.get("recoveryReason").toString() : "none"; + groupId = row.get("groupId") != null ? row.get("groupId").toString() : null; + cage = row.get("cage") != null ? row.get("cage").toString() : null; + location = row.get("location") != null ? row.get("location").toString() : null; + room = row.get("room") != null ? row.get("room").toString() : null; + } + // Gets passed-in review required status. + String reviewRequired = row.get("reviewRequired") != null ? row.get("reviewRequired").toString() : null; + Boolean reviewRequiredParsed = reviewRequired != null ? Boolean.parseBoolean(reviewRequired) : false; + Integer rowQcState = reviewRequiredParsed == true ? EHRService.QCSTATES.ReviewRequired.getQCState(getContainer()).getRowId() : EHRService.QCSTATES.Completed.getQCState(getContainer()).getRowId(); // Sets data to new row variable. Map validatedRow = new HashMap<>(); validatedRow.put("Id", id); - validatedRow.put("date", serverDate); + validatedRow.put("date", Timestamp.valueOf(serverDate)); validatedRow.put("recoveryReason", recoveryReason); validatedRow.put("observer", observer); validatedRow.put("recoveryId", recoveryId); @@ -2558,10 +2585,11 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep validatedRow.put("cage", cage); validatedRow.put("cageLockSecure", cageLockSecure); validatedRow.put("deviceId", deviceId); + validatedRow.put("QCState", rowQcState); - // 4a. Check row for required fields (doesn't require initials for a row deletion). + // 4a. Check row for required fields. String[] requiredFields = { - "Id", "recoveryReason", "observer", "recoveryId", "observation", "submitterInitials" + "Id", "observer", "recoveryId", "observation", "submitterInitials" }; boolean missingField = false; for (String field : requiredFields) { @@ -2575,22 +2603,9 @@ public Object execute(SimpleApiJsonForm form, BindException errors) throws Excep continue; // Invalid row; skip adding to batch update and continue checking other rows. } - // 4b. Verifies animal is currently alive and exists at center. - SimpleFilter existsAliveAtCenterFilter = new SimpleFilter("id", id, CompareType.EQUAL); - String[] existsAliveAtCenterTargetColumns = new String[]{"Id", "calculated_status"}; - ArrayList> existsAliveAtCenterRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", existsAliveAtCenterFilter, null, existsAliveAtCenterTargetColumns); - if (existsAliveAtCenterRows == null || existsAliveAtCenterRows.isEmpty()) { - batchErrors.addRowError((new ValidationException("Animal does not currently exist at the center."))); - continue; // Invalid row; skip adding to batch update and continue checking other rows. - } - else if (!existsAliveAtCenterRows.get(0).get("calculated_status").equals("Alive")) { - batchErrors.addRowError((new ValidationException("Animal is no longer alive."))); - continue; // Invalid row; skip adding to batch update and continue checking other rows. - } - - // 4c. Verifies no other active recoveries exist for the current animal (only if this is an 'import' observation). + // 4b. Validate existing table data before updating (depending on observation being added). if (observation.equals("Imported")) { - // Get started & finished recoveries. + // Verify no other active recoveries exist for the current animal. String[] existingRecoveriesTargetColumn = new String[]{"recoveryId"}; SimpleFilter recoveriesStartedFilter = new SimpleFilter("id", id, CompareType.EQUAL); recoveriesStartedFilter.addCondition("observation", "Imported", CompareType.EQUAL); @@ -2603,9 +2618,74 @@ else if (!existsAliveAtCenterRows.get(0).get("calculated_status").equals("Alive" batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before beginning a new one."))); continue; // Invalid row; skip adding to batch update and continue checking other rows. } + else if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows == null) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before beginning a new one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + // Verifies animal is currently alive and exists at center. + SimpleFilter existsAliveAtCenterFilter = new SimpleFilter("id", id, CompareType.EQUAL); + String[] existsAliveAtCenterTargetColumns = new String[]{"Id", "calculated_status"}; + ArrayList> existsAliveAtCenterRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", existsAliveAtCenterFilter, null, existsAliveAtCenterTargetColumns); + if (existsAliveAtCenterRows == null || existsAliveAtCenterRows.isEmpty()) { + batchErrors.addRowError((new ValidationException("Animal does not currently exist at the center."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else if (!existsAliveAtCenterRows.get(0).get("calculated_status").equals("Alive")) { + batchErrors.addRowError((new ValidationException("Animal is no longer alive."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + } + else if (observation.equals("Deleted")) { + // Verify only 1 row exists with this 'recoveryId' and has the status of 'Imported', then retrieve the lsid. + String[] existingRecoveryLsidColumn = new String[]{"lsid"}; + SimpleFilter existingRecoveryFilter = new SimpleFilter("recoveryId", recoveryId, CompareType.EQUAL); + ArrayList> existingRecoveryRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", existingRecoveryFilter, null, existingRecoveryLsidColumn); + if (existingRecoveryRows == null || existingRecoveryRows.isEmpty()) { + batchErrors.addRowError((new ValidationException("Recovery: " + recoveryId + " has no data to delete."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + if (existingRecoveryRows.size() > 1) { + batchErrors.addRowError((new ValidationException("Recovery: " + recoveryId + " has already had 1 or more observations after 'Imported'. In-progress recoveries cannot be deleted."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else { + validatedRow.put("lsid", existingRecoveryRows.get(0).get("lsid")); + } } + else if (observation.equals("Unfinalized")) { + // Verify this animal has no other open recoveries currently active. + String[] existingRecoveriesTargetColumn = new String[]{"recoveryId"}; + SimpleFilter recoveriesStartedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesStartedFilter.addCondition("observation", "Imported", CompareType.EQUAL); + SimpleFilter recoveriesFinishedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesFinishedFilter.addCondition("observation", "Fully Recovered;Deleted", CompareType.IN); + ArrayList> recoveriesStartedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesStartedFilter, null, existingRecoveriesTargetColumn); + ArrayList> recoveriesFinishedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesFinishedFilter, null, existingRecoveriesTargetColumn); + // Verify counts match. + if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows != null && !recoveriesFinishedRows.isEmpty() && recoveriesStartedRows.size() != recoveriesFinishedRows.size()) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before restoring a previous one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows == null) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before restoring a previous one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } - // 4d. Create or Update task into task dataset. + // Verify a row exists with this 'recoveryId' and a status of 'Fully Recovered', then retrieve the lsid. + String[] existingRecoveryLsidColumn = new String[]{"lsid"}; + SimpleFilter existingFinalizedRecoveryFilter = new SimpleFilter("recoveryId", recoveryId, CompareType.EQUAL); + existingFinalizedRecoveryFilter.addCondition("observation", "Fully Recovered", CompareType.EQUAL); + ArrayList> existingRecoveryRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", existingFinalizedRecoveryFilter, null, existingRecoveryLsidColumn); + if (existingRecoveryRows == null || existingRecoveryRows.isEmpty()) { + batchErrors.addRowError((new ValidationException("Recovery: " + recoveryId + " has no recovery data to resume."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else { + validatedRow.put("lsid", existingRecoveryRows.get(0).get("lsid")); + } + } + + // 4c. Create or Update task into task dataset. Map taskRecord = new HashMap<>(); taskRecord.put("taskid", recoveryId); taskRecord.put("title", "Anesthesia Recovery"); @@ -2617,6 +2697,7 @@ else if (!existsAliveAtCenterRows.get(0).get("calculated_status").equals("Alive" tasksToInsert.add(taskRecord); } else if (observation.equals("Sitting Upright") || observation.equals("Laying Down")) { +// taskRecord.put("qcstate", qcStateStarted); taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); tasksToUpdate.add(taskRecord); } @@ -2628,13 +2709,19 @@ else if (observation.equals("Deleted")) { taskRecord.put("qcstate", EHRService.QCSTATES.DeleteRequested.getQCState(getContainer()).getRowId()); tasksToUpdate.add(taskRecord); } + else if (observation.equals("Unfinalized")) { + taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); + tasksToUpdate.add(taskRecord); + } - // 4e. Current row is valid, added to validatedRows for the batch update. - validatedRows.add(validatedRow); - + // 4e. Current row is valid, added to rowsToInsert for the batch update. + if (observation.equals("Unfinalized") || observation.equals("Deleted")) { + rowsToDelete.add(validatedRow); + } else { + rowsToInsert.add(validatedRow); + } } - // 6. Report all manual validation errors back to the user. if (batchErrors.hasErrors()) { String combinedErrors = batchErrors.getRowErrors().stream() @@ -2654,31 +2741,35 @@ else if (observation.equals("Deleted")) { QueryUpdateService anesthesiaTableService = anesthesiaTableInfo.getUpdateService(); QueryUpdateService tasksTableService = tasksTableInfo.getUpdateService(); BatchValidationException dbErrors = new BatchValidationException(); - validatedRows = SimpleQueryUpdater.makeRowListCaseInsensitive(validatedRows); + rowsToInsert = SimpleQueryUpdater.makeRowListCaseInsensitive(rowsToInsert); + rowsToDelete = SimpleQueryUpdater.makeRowListCaseInsensitive(rowsToDelete); tasksToInsert = SimpleQueryUpdater.makeRowListCaseInsensitive(tasksToInsert); tasksToUpdate = SimpleQueryUpdater.makeRowListCaseInsensitive(tasksToUpdate); // 7b. Creates the tasks containing all rows to be inserted. - List> anesthesiaRowsToInsert = anesthesiaTableService.insertRows(getUser(), getContainer(), validatedRows, dbErrors, null, null); + List> anesthesiaRowsToInsert = anesthesiaTableService.insertRows(getUser(), getContainer(), rowsToInsert, dbErrors, null, null); + // TODO: Check with labkey to verify there's no dbErrors to add to the anesthesiaRowsToDelete object below. Row deletion is not done in batch so this will work regardless. + List> anesthesiaRowsToDelete = anesthesiaTableService.deleteRows(getUser(), getContainer(), rowsToDelete, null, null); tasksTableService.insertRows(getUser(), getContainer(), tasksToInsert, dbErrors, null, null); tasksTableService.updateRows(getUser(), getContainer(), tasksToUpdate, tasksToUpdate, dbErrors, null, null); - // 7c. Checks for errors in any row and aborts the insert + // 7c. Checks for errors in any row and aborts the insert. if (dbErrors.hasErrors()) { throw dbErrors; } // 7d. Executes the Task to insert all rows via a batch transaction. transaction.commit(); - response.put("success", true);; + response.put("success", true); response.put("detailedResponse", "Database save successful."); - response.put("rowsUpdated", anesthesiaRowsToInsert.size()); + response.put("rowsInserted", anesthesiaRowsToInsert.size()); + response.put("rowsUpdated", anesthesiaRowsToDelete.size()); return response; } - // Catches any errors + // Catches any errors. catch (Exception e) { - _log.info("There was an issue inserting rows into the anesthesiaRecovery dataset: " + e.getMessage()); + _log.info("There was an issue modifying the anesthesiaRecovery dataset: " + e.getMessage()); response.put("success", false); response.put("detailedResponse", "Database save failed: " + e.getMessage()); return response; diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java index 538bb31b0..3e39d0475 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java @@ -52,9 +52,9 @@ public String getEmailSubject(Container c) { return "Anesthesia Recovery Review: " + dateToolkit.getCurrentTime(); } @Override - public String getScheduleDescription() { return "Daily at 3:00PM"; } + public String getScheduleDescription() { return "Daily at 1:00PM and 3:00PM"; } @Override - public String getCronString() { return notificationToolkit.createCronString("0", "15", "*"); } + public String getCronString() { return notificationToolkit.createCronString("0", "13,15", "*"); } @Override public String getCategory() { return "iOS App Notifications"; } @@ -67,6 +67,7 @@ public String getMessageBodyHTML(Container c, User u) { // Creates variables & gets data. final StringBuilder messageBody = new StringBuilder(); AnesthesiaRecoveryReviewNotificationObject myRecoveriesObject = new AnesthesiaRecoveryReviewNotificationObject(c, u); + AnesthesiaRecoveryReviewReviewRequiredObject myRequiredReviewsObject = new AnesthesiaRecoveryReviewReviewRequiredObject(c, u); // Creates CSS. messageBody.append(styleToolkit.beginStyle()); @@ -75,19 +76,31 @@ public String getMessageBodyHTML(Container c, User u) { messageBody.append(styleToolkit.endStyle()); // Begins message info. - messageBody.append("

This email contains all unclosed anesthesia recoveries. It was run on: " + dateToolkit.getCurrentTime() + "

"); + messageBody.append("

This email contains any issues with the Anesthesia Recovery dataset. It was run on: " + dateToolkit.getCurrentTime() + "

"); // Creates table. - if (myRecoveriesObject.unclosedRecoveries.isEmpty()) { + if (myRecoveriesObject.unclosedRecoveries.isEmpty() && myRequiredReviewsObject.reviewRequiredRecoveries.isEmpty()) { +// messageBody.append("All anesthesia recoveries have been closed and no reviews are needed."); // TODO: Use this if users want emails to still send when all recoveries are closed. notificationToolkit.sendEmptyNotificationRevamp(c, u, "Anesthesia Recovery Review"); return null; -// messageBody.append("All anesthesia recoveries have been closed."); // TODO: Use this if users want emails to still send when all recoveries are closed. } else { - for (HashMap result : myRecoveriesObject.unclosedRecoveries) { - messageBody.append(result.get("Id") + "
"); + if (!myRecoveriesObject.unclosedRecoveries.isEmpty()) { + messageBody.append("The following recoveries are still open and have not been closed yet:"); + for (HashMap result : myRecoveriesObject.unclosedRecoveries) { + messageBody.append(result.get("Id") + "
"); + } + messageBody.append(notificationToolkit.createHyperlink("Click here to view all unclosed recoveries


", myRecoveriesObject.unclosedRecoveriesURL)); + } + if (!myRequiredReviewsObject.reviewRequiredRecoveries.isEmpty()) + { + messageBody.append("The following recoveries have been flagged as 'Review Required':"); + for (HashMap result : myRequiredReviewsObject.reviewRequiredRecoveries) + { + messageBody.append(result.get("Id") + "
"); + } + messageBody.append(notificationToolkit.createHyperlink("Click here to view all 'Review Required' recoveries


", myRequiredReviewsObject.reviewRequiredRecoveriesURL)); } - messageBody.append(notificationToolkit.createHyperlink("Click here to view recoveries


", myRecoveriesObject.unclosedRecoveriesURL)); } // Returns message. @@ -146,4 +159,46 @@ private void getUnclosedAnesthesiaRecoveries() { this.unclosedRecoveriesURL = viewQueryURL; } } + + public static class AnesthesiaRecoveryReviewReviewRequiredObject { + Container c; + User u; + NotificationToolkit notificationToolkit = new NotificationToolkit(); + NotificationToolkit.DateToolkit dateToolkit = new NotificationToolkit.DateToolkit(); + + // Constructor function. + public AnesthesiaRecoveryReviewReviewRequiredObject(Container currentContainer, User currentUser) { + this.c = currentContainer; + this.u = currentUser; + this.getRecoveriesWithReviewRequired(); + } + + // Find all anesthesia recoveries that have been opened, but not closed. + ArrayList> reviewRequiredRecoveries; + String reviewRequiredRecoveriesURL; + private void getRecoveriesWithReviewRequired() { + // Creates filter. + SimpleFilter reviewRequiredFilter = new SimpleFilter("qcstate/label", "Review Required", CompareType.EQUAL); + // Creates sort. + Sort mySort = new Sort("Id"); + // Creates columns to retrieve. + String[] targetColumns = new String[]{"Id", "recoveryId"}; // TODO: Change this to task after implementing TaskID (only needed if we remove recoveryId). + // Runs query. + ArrayList> openedArray = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(c, u, "study", "anesthesiaRecovery", reviewRequiredFilter, mySort, targetColumns); + + // 1. Extract recoveryIds from closedArray into a string list (for query filtering below). + List reviewRequiredIds = openedArray.stream() + .map(map -> map.get("recoveryId")) + .filter(Objects::nonNull) + .toList(); + // 2. Creates a URL consisting of all review required ID's. CompareType.IN requries a semicolon separated string list. + String reviewRequiredIdsAsString = String.join(";", reviewRequiredIds); + SimpleFilter unclosedFilter = new SimpleFilter("recoveryId", reviewRequiredIdsAsString, CompareType.IN); + String viewQueryURL = notificationToolkit.createQueryURL(c, "execute", "study", "anesthesiaRecovery", unclosedFilter); + + // Returns data. + this.reviewRequiredRecoveries = new ArrayList<>(openedArray); + this.reviewRequiredRecoveriesURL = viewQueryURL; + } + } } diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java index aa17b4895..85274d963 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/table/WNPRC_EHRCustomizer.java @@ -321,7 +321,7 @@ private void customizeFeedingTable(AbstractTableInfo ti) } private void customizeAnesthesiaRecoveryTable(AbstractTableInfo ti) { - // Defines new customized column and display name. + // Defines new 'start time' customized column and display name. String recoveryStartTimeColumnName = "recoveryStartTime"; String recoveryStartTimeDisplayName = "Recovery Start Time"; String recoveryStartTimeDescription = "This column shows the calculated original start time for this specific recovery (the date/time the first observation was made)."; @@ -337,6 +337,150 @@ private void customizeAnesthesiaRecoveryTable(AbstractTableInfo ti) { newCol.setLabel(recoveryStartTimeDisplayName); newCol.setDescription(recoveryStartTimeDescription); ti.addColumn(newCol); + + // Defines new 'taskId' customized column and display name. + String taskIdColumnName = "taskid"; + String taskIdDisplayName = "Task ID"; + String taskIdDescription = "This column shows the task corresponding to this row's recoveryId."; + // Creates SQL script to define what to show. + SQLFragment taskIdSql = new SQLFragment(ExprColumn.STR_TABLE_ALIAS + ".recoveryId"); + // Compiles data and creates new column to insert. + ExprColumn taskIdColumn = new ExprColumn(ti, taskIdColumnName, taskIdSql, JdbcType.VARCHAR); + taskIdColumn.setLabel(taskIdDisplayName); + taskIdColumn.setDescription(taskIdDescription); + ti.addColumn(taskIdColumn); + + // Defines new 'recovery reason' customized column and display name, then sets the value so all entries match data in the 'Imported' row. + String recoveryReasonColumnName = "recoveryReasonFinal"; + String recoveryReasonDisplayName = "Recovery Reason Final"; + String recoveryReasonDescription = "This column shows the calculated original recovery reason for this specific recovery (the recoveryReason assigned when the recovery was first imported)."; + // Creates SQL script to define what to show in column. + SQLFragment recoveryReasonSql = new SQLFragment("(SELECT sub.recoveryReason FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation = 'Imported' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId") + .append(" ORDER BY sub.created ASC LIMIT 1)"); + // Compiles data and creates the new column to insert. + ExprColumn recoveryReasonColumn = new ExprColumn(ti, recoveryReasonColumnName, recoveryReasonSql, JdbcType.VARCHAR); + recoveryReasonColumn.setLabel(recoveryReasonDisplayName); + recoveryReasonColumn.setDescription(recoveryReasonDescription); + ti.addColumn(recoveryReasonColumn); + + // Defines new 'cage final' customized column and display name, then sets the value so all entries match data in the 'Imported' row. + String cageColumnName = "cageFinal"; + String cageDisplayName = "Cage Final"; + String cageDescription = "This column shows the calculated original cage for this specific recovery (the cage assigned when the recovery was first imported)."; + // Creates SQL script to define what to show in column. + SQLFragment cageSql = new SQLFragment("(SELECT sub.cage FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation = 'Imported' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId") + .append(" ORDER BY sub.created ASC LIMIT 1)"); + // Compiles data and creates the new column to insert. + ExprColumn cageColumn = new ExprColumn(ti, cageColumnName, cageSql, JdbcType.VARCHAR); + recoveryReasonColumn.setLabel(cageDisplayName); + recoveryReasonColumn.setDescription(cageDescription); + ti.addColumn(cageColumn); + + // Defines new 'location final' customized column and display name, then sets the value so all entries match data in the 'Imported' row. + String locationColumnName = "locationFinal"; + String locationDisplayName = "Location Final"; + String locationDescription = "This column shows the calculated original description for this specific recovery (the description assigned when the recovery was first imported)."; + // Creates SQL script to define what to show in column. + SQLFragment locationSql = new SQLFragment("(SELECT sub.location FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation = 'Imported' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId") + .append(" ORDER BY sub.created ASC LIMIT 1)"); + // Compiles data and creates the new column to insert. + ExprColumn locationColumn = new ExprColumn(ti, locationColumnName, locationSql, JdbcType.VARCHAR); + recoveryReasonColumn.setLabel(locationDisplayName); + recoveryReasonColumn.setDescription(locationDescription); + ti.addColumn(locationColumn); + + // Defines new 'room final' customized column and display name, then sets the value so all entries match data in the 'Imported' row. + String roomColumnName = "roomFinal"; + String roomDisplayName = "Room Final"; + String roomDescription = "This column shows the calculated original room for this specific recovery (the room assigned when the recovery was first imported)."; + // Creates SQL script to define what to show in column. + SQLFragment roomSql = new SQLFragment("(SELECT sub.room FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation = 'Imported' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId") + .append(" ORDER BY sub.created ASC LIMIT 1)"); + // Compiles data and creates the new column to insert. + ExprColumn roomColumn = new ExprColumn(ti, roomColumnName, roomSql, JdbcType.VARCHAR); + recoveryReasonColumn.setLabel(roomDisplayName); + recoveryReasonColumn.setDescription(roomDescription); + ti.addColumn(roomColumn); + + + + + + // Defines new 'group id' customized column and display name, then sets the value so all entries match data in the 'Imported' row. + String groupIdColumnName = "groupIdFinal"; + String groupIdDisplayName = "Group ID Final"; + String groupIdDescription = "This column shows the calculated original group ID for this specific recovery (the groupID assigned when the first observation was made)."; + // Creates SQL script to define what to show in column. + SQLFragment groupIdSql = new SQLFragment("(SELECT sub.groupId FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation = 'Imported' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId") + .append(" ORDER BY sub.created ASC LIMIT 1)"); + // Compiles data and creates the new column to insert. + ExprColumn groupIdCol = new ExprColumn(ti, groupIdColumnName, groupIdSql, JdbcType.VARCHAR); + groupIdCol.setLabel(groupIdDisplayName); + groupIdCol.setDescription(groupIdDescription); + ti.addColumn(groupIdCol); + + // Defines new 'total recovery time' customized column and display name. + String totalRecoveryTimeName = "totalRecoveryTime"; + String totalRecoveryTimeDisplayName = "Total Recovery Time"; + String totalRecoveryTimeDescription = "This column shows the calculated total recovery time for this specific recovery."; + // Creates SQL script to define what to show in column. + SQLFragment totalRecoveryTimeSql = new SQLFragment("EXTRACT(EPOCH FROM (") + // End Time: 'Fully Recovered'. + .append("(SELECT sub.date FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation = 'Fully Recovered' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId) - ") + // Start Time: First non-'Imported' observation. + .append("(SELECT MIN(sub.date) FROM ") + .append(ti.getFromSQL("sub")) + .append(" WHERE sub.observation != 'Imported' AND sub.recoveryId = ") + .append(ExprColumn.STR_TABLE_ALIAS) + .append(".recoveryId)") + .append(")) / 60"); + // Compiles data and creates the new column. + ExprColumn totalRecoveryTimeCol = new ExprColumn(ti, totalRecoveryTimeName, totalRecoveryTimeSql, JdbcType.DOUBLE); + totalRecoveryTimeCol.setLabel(totalRecoveryTimeDisplayName); + totalRecoveryTimeCol.setDescription(totalRecoveryTimeDescription); + // Creates a display renderer. + totalRecoveryTimeCol.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo) { + @Override + public @NotNull HtmlString getFormattedHtml(RenderContext ctx) { + Object value = getValue(ctx); + if (value == null) return HtmlString.EMPTY_STRING; + double totalMinutes = ((Number) value).doubleValue(); + long hours = (long) (totalMinutes / 60); + long minutes = Math.round(totalMinutes % 60); + // Handle edge case where rounding minutes up hits 60. + if (minutes == 60) { + hours += 1; + minutes = 0; + } + return HtmlString.of(String.format("%d:%02d", hours, minutes)); + } + }); + + ti.addColumn(totalRecoveryTimeCol); } private void customizeSessionLogTable(AbstractTableInfo ti) { From b161466078bfa0b0bde22c584545bc13245b36bb Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Tue, 4 Aug 2026 14:48:32 -0500 Subject: [PATCH 10/12] Added all necessary files from external module. These are required for wnprc_ios_module to exist. --- .../study/anesthesiaRecovery.query.xml | 2 +- .../study/anesthesiaRecovery/.qview.xml | 34 ++- wnprc_ios_app/.gitignore | 1 + wnprc_ios_app/README.md | 0 wnprc_ios_app/build.gradle | 5 + wnprc_ios_app/module.properties | 6 + .../postgresql/wnprc_ios_app-0.000-25.000.sql | 129 +++++++++ .../resources/schemas/wnprc_ios_app.xml | 83 ++++++ wnprc_ios_app/resources/views/begin.html | 1 + .../wnprc_ios_appContainerListener.java | 55 ++++ .../wnprc_ios_appController.java | 272 ++++++++++++++++++ .../wnprc_ios_app/wnprc_ios_appManager.java | 32 +++ .../wnprc_ios_app/wnprc_ios_appModule.java | 112 ++++++++ .../wnprc_ios_app/wnprc_ios_appSchema.java | 74 +++++ 14 files changed, 790 insertions(+), 16 deletions(-) create mode 100644 wnprc_ios_app/.gitignore create mode 100644 wnprc_ios_app/README.md create mode 100644 wnprc_ios_app/build.gradle create mode 100644 wnprc_ios_app/module.properties create mode 100644 wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-25.000.sql create mode 100644 wnprc_ios_app/resources/schemas/wnprc_ios_app.xml create mode 100644 wnprc_ios_app/resources/views/begin.html create mode 100644 wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appContainerListener.java create mode 100644 wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appController.java create mode 100644 wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appManager.java create mode 100644 wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appModule.java create mode 100644 wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appSchema.java diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml index 6777c912e..3b2684fbd 100644 --- a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml @@ -14,7 +14,7 @@
- Recovery Start Time + Recovery Start Time Final diff --git a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml index 5a1dd4ac6..971cfb1b3 100644 --- a/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml +++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml @@ -4,30 +4,34 @@ - - - - - - + + + + - - - - - - + + + + + + + - - - + + + + + + + + diff --git a/wnprc_ios_app/.gitignore b/wnprc_ios_app/.gitignore new file mode 100644 index 000000000..e43b0f988 --- /dev/null +++ b/wnprc_ios_app/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/wnprc_ios_app/README.md b/wnprc_ios_app/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/wnprc_ios_app/build.gradle b/wnprc_ios_app/build.gradle new file mode 100644 index 000000000..07df36599 --- /dev/null +++ b/wnprc_ios_app/build.gradle @@ -0,0 +1,5 @@ +import org.labkey.gradle.util.BuildUtils + +plugins { + id 'org.labkey.build.module' +} diff --git a/wnprc_ios_app/module.properties b/wnprc_ios_app/module.properties new file mode 100644 index 000000000..0b85a7ee0 --- /dev/null +++ b/wnprc_ios_app/module.properties @@ -0,0 +1,6 @@ +ModuleClass: org.labkey.wnprc_ios_app.wnprc_ios_appModule +Name: wnprc_ios_app +ManageVersion: false +SupportedDatabases: pgsql +#SchemaVersion: 25.000 +BuildType: Production \ No newline at end of file diff --git a/wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-25.000.sql b/wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-25.000.sql new file mode 100644 index 000000000..cd1a40d1a --- /dev/null +++ b/wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-25.000.sql @@ -0,0 +1,129 @@ +-- This tracks the official SQL script that will be used when migrating to production for the first time. + +--Creates the schema to hold tables for the wnprc ios app. +CREATE SCHEMA IF NOT EXISTS wnprc_ios_app; + + + + + +-- Creates 'Reported Issues' dataset. +DROP TABLE IF EXISTS wnprc_ios_app.reported_issues; +CREATE TABLE wnprc_ios_app.reported_issues ( + -- Default LabKey fields. + rowid serial NOT NULL, + container entityid NOT NULL, + createdby userid NOT NULL, + created TIMESTAMP NOT NULL, + modifiedby userid NOT NULL, + modified TIMESTAMP NOT NULL, + + -- Issue details. + issue_description varchar(4000) NOT NULL, + + -- Dev details. + dev_comments varchar(4000), + status varchar(100) NOT NULL, + + -- Primary key. + CONSTRAINT PK_reported_issues PRIMARY KEY (rowid) +); + + + + + +-- Creates 'Animal Abstract Preferences' dataset. +DROP TABLE IF EXISTS wnprc_ios_app.user_animal_abstract_preferences; +CREATE TABLE wnprc_ios_app.user_animal_abstract_preferences ( +-- Default LabKey fields. +container entityid NOT NULL, +createdby userid NOT NULL, +created TIMESTAMP NOT NULL, +modifiedby userid NOT NULL, +modified TIMESTAMP NOT NULL, + +-- Preferences. +show_id BOOLEAN NOT NULL DEFAULT TRUE, +show_gender BOOLEAN NOT NULL DEFAULT TRUE, +show_availability BOOLEAN NOT NULL DEFAULT TRUE, +show_room BOOLEAN NOT NULL DEFAULT TRUE, +show_cage BOOLEAN NOT NULL DEFAULT TRUE, +show_condition BOOLEAN NOT NULL DEFAULT TRUE, +show_num_animals_in_cage BOOLEAN NOT NULL DEFAULT TRUE, +show_status BOOLEAN NOT NULL DEFAULT TRUE, +show_age BOOLEAN NOT NULL DEFAULT TRUE, +show_birth BOOLEAN NOT NULL DEFAULT TRUE, +show_dam BOOLEAN NOT NULL DEFAULT TRUE, +show_sire BOOLEAN NOT NULL DEFAULT TRUE, +show_tb_date BOOLEAN NOT NULL DEFAULT TRUE, +show_prepaid BOOLEAN NOT NULL DEFAULT TRUE, +show_mgap_ids BOOLEAN NOT NULL DEFAULT TRUE, +show_most_recent_weight BOOLEAN NOT NULL DEFAULT TRUE, +show_most_recent_weight_date BOOLEAN NOT NULL DEFAULT TRUE, +show_hold BOOLEAN NOT NULL DEFAULT TRUE, +show_medical BOOLEAN NOT NULL DEFAULT TRUE, +show_current_behaviors BOOLEAN NOT NULL DEFAULT TRUE, +show_most_recent_alopecia_score BOOLEAN NOT NULL DEFAULT TRUE, +show_most_recent_body_condition_score BOOLEAN NOT NULL DEFAULT TRUE, +show_origin BOOLEAN NOT NULL DEFAULT TRUE, +show_geographic_origin BOOLEAN NOT NULL DEFAULT TRUE, +show_ancestry BOOLEAN NOT NULL DEFAULT TRUE, +show_most_recent_arrival BOOLEAN NOT NULL DEFAULT TRUE, +show_most_recent_departure BOOLEAN NOT NULL DEFAULT TRUE, +show_death BOOLEAN NOT NULL DEFAULT TRUE, +show_remark BOOLEAN NOT NULL DEFAULT TRUE, +show_mgap_sequence_types BOOLEAN NOT NULL DEFAULT TRUE, + +-- Primary key. +target_user userid NOT NULL, +CONSTRAINT PK_user_animal_abstract_preferences PRIMARY KEY (target_user) +); + + + + + +-- Creates 'Session Log' dataset. +DROP TABLE IF EXISTS wnprc_ios_app.session_log; +CREATE TABLE wnprc_ios_app.session_log ( +-- Default LabKey fields. +container entityid NOT NULL, +createdby userid NOT NULL, +created TIMESTAMP NOT NULL, +modifiedby userid NOT NULL, +modified TIMESTAMP NOT NULL, + +-- Request details. +original_row_id varchar(100) NOT NULL, +query_name varchar(4000) NOT NULL, +request_type varchar(100) NOT NULL, +errors_occurred BOOLEAN NOT NULL DEFAULT FALSE, +dev_comments varchar(4000), +error_description varchar(4000), + +-- Primary key. +CONSTRAINT PK_wnprc_ios_app PRIMARY KEY (original_row_id) +); + + + + + +-- Creates 'Push Notifications' dataset. +DROP TABLE IF EXISTS wnprc_ios_app.push_notifications; +CREATE TABLE wnprc_ios_app.push_notifications ( +-- Default LabKey fields. +container entityid NOT NULL, +createdby userid NOT NULL, +created TIMESTAMP NOT NULL, +modifiedby userid NOT NULL, +modified TIMESTAMP NOT NULL, + +-- Push details. +push_token varchar(4000), + +-- Primary key. +target_user userid NOT NULL, +CONSTRAINT PK_push_notifications PRIMARY KEY (target_user) +); diff --git a/wnprc_ios_app/resources/schemas/wnprc_ios_app.xml b/wnprc_ios_app/resources/schemas/wnprc_ios_app.xml new file mode 100644 index 000000000..2efd9a22f --- /dev/null +++ b/wnprc_ios_app/resources/schemas/wnprc_ios_app.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
\ No newline at end of file diff --git a/wnprc_ios_app/resources/views/begin.html b/wnprc_ios_app/resources/views/begin.html new file mode 100644 index 000000000..16f2c823f --- /dev/null +++ b/wnprc_ios_app/resources/views/begin.html @@ -0,0 +1 @@ +

PrimatePal admin page will be shown here.

\ No newline at end of file diff --git a/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appContainerListener.java b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appContainerListener.java new file mode 100644 index 000000000..5c3b53e86 --- /dev/null +++ b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appContainerListener.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 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.wnprc_ios_app; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager.ContainerListener; +import org.labkey.api.security.User; +import java.util.Collections; +import java.util.Collection; + +import java.beans.PropertyChangeEvent; + +public class wnprc_ios_appContainerListener implements ContainerListener +{ + @Override + public void containerCreated(Container c, User user) + { + } + + @Override + public void containerDeleted(Container c, User user) + { + } + + @Override + public void propertyChange(PropertyChangeEvent evt) + { + } + + @Override + public void containerMoved(Container c, Container oldParent, User user) + { + } + + @NotNull @Override + public Collection canMove(Container c, Container newParent, User user) + { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appController.java b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appController.java new file mode 100644 index 000000000..dc242fd6e --- /dev/null +++ b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appController.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2025 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.wnprc_ios_app; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONObject; +import org.labkey.api.action.ApiSimpleResponse; +import org.labkey.api.action.MutatingApiAction; +import org.labkey.api.action.SimpleApiJsonForm; +import org.labkey.api.action.SimpleViewAction; +import org.labkey.api.action.SpringActionController; +import org.labkey.api.data.CompareType; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.TableSelector; +import org.labkey.api.query.QueryService; +import org.labkey.api.security.ActionNames; +import org.labkey.api.security.RequiresLogin; +import org.labkey.api.security.RequiresPermission; +import org.labkey.api.security.permissions.ReadPermission; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.JspView; +import org.labkey.api.view.NavTree; +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.List; + +public class wnprc_ios_appController extends SpringActionController +{ + private static Logger _log = LogManager.getLogger(wnprc_ios_appController.class); + private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(wnprc_ios_appController.class); + public static final String NAME = "wnprc_ios_app"; + + public wnprc_ios_appController() + { + setActionResolver(_actionResolver); + } + +// @RequiresPermission(ReadPermission.class) +// public class BeginAction extends SimpleViewAction +// { +// public ModelAndView getView(Object o, BindException errors) +// { +// return new JspView("/org/labkey/wnprc_ios_app/view/hello.jsp"); +// } +// +// public void addNavTrail(NavTree root) { } +// } + + + +// // This function is muted until needed. +// // This function always returns 'true'. It is a placeholder if we decide to implement any future validation for this dataset. +// @ActionNames("updatePushNotification") +//// @RequiresLogin +// @RequiresPermission(ReadPermission.class) +// public static class UpdatePushNotificationAction extends MutatingApiAction { +// @Override +// public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception { +// // Creates variables. +// ApiSimpleResponse response = new ApiSimpleResponse(); +// +// // Retrieves passed-in arguments. +// JSONObject myForm = form.getJsonObject(); +// String pushToken = myForm.getString("push_token").toString(); +// String targetUser = myForm.getString("target_user").toString(); +// String insertOrUpdate = myForm.getString("insert_or_update").toString(); +// +// // Code any necessary validation here. +// // Verifies user is either inserting (with no previous preferences) or updating (with previous preferences). +// SimpleFilter pushNotificationsFilter = new SimpleFilter("target_user", targetUser, CompareType.EQUAL); +// TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "wnprc_ios_app").getTable("push_notifications"); +// TableSelector myTable = new TableSelector(ti, PageFlowUtil.set("target_user"), pushNotificationsFilter, null); +// Map[] rows = myTable.getMapArray(); +// if (insertOrUpdate.equals("insert")) { +// if (rows.length > 0) { +// response.put("detailedResponse", "UpdatePushNotificationAction API: Insert failed due to multiple rows existing."); +// response.put("success", false); +// return response; +// } +// } +// else if (insertOrUpdate.equals("update")) { +// if (rows.length != 1) { +// response.put("detailedResponse", "UpdatePushNotificationAction API: Update failed due to not having 1 existing row."); +// response.put("success", false); +// return response; +// } +// } +// +// // Successfully completes validation. +// _log.info("UpdatePushNotificationAction API: pushToken=" + pushToken + ", targetUser=" + targetUser + ", insertOrUpdate=" + insertOrUpdate); +// response.put("detailedResponse", "UpdatePushNotificationAction API: Push notification record validated successfully."); +// response.put("success", true); +// return response; +// } +// } + + + +// // This function is muted until needed. +// // This function always returns 'true'. It is a placeholder if we decide to implement any future validation for this dataset. +// @RequiresLogin +// public static class UpdateReportedIssuesAction extends MutatingApiAction { +// @Override +// public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception { +// // Creates variables. +// ApiSimpleResponse response = new ApiSimpleResponse(); +// +// // Retrieves passed-in arguments. +// JSONObject myForm = form.getJsonObject(); +// String issueDescription = myForm.getString("issue_description").toString(); +// String issueStatus = myForm.getString("status").toString(); +// +// _log.info("UpdateReportedIssuesAction API: issueDescription=" + issueDescription + ", issueStatus=" + issueStatus); +// // Code any necessary validation here. +// +// response.put("detailedResponse", "UpdateReportedIssuesAction API: Reported issue validated successfully."); +// response.put("success", true); +// return response; +// } +// } + + + +// // This function is muted until needed. +// // This function always returns 'true'. It is a placeholder if we decide to implement any future validation for this dataset. +// @RequiresLogin +// public static class UpdateSessionLogAction extends MutatingApiAction +// { +// @Override +// public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception +// { +// // Creates variables. +// ApiSimpleResponse response = new ApiSimpleResponse(); +// +// // Retrieves passed-in arguments. +// JSONObject myForm = form.getJsonObject(); +// String startTime = myForm.getString("start_time").toString(); +// String endTime = myForm.getString("end_time").toString(); +// String schemaName = myForm.getString("schema_name").toString(); +// String queryName = myForm.getString("query_name").toString(); +// String numberOfRecords = myForm.getString("number_of_records").toString(); +// String errorsOccurred = myForm.getString("errors_occurred").toString(); +// String createdBy = myForm.getString("createdby").toString(); +// String userAgent = myForm.getString("user_agent").toString(); +// +// _log.info("UpdateSessionLog API: startTime=" + startTime + ", endTime=" + endTime + ", schemaName=" + schemaName + ", queryName=" + queryName + ", numberOfRecords=" + numberOfRecords + ", errorsOccurred=" + errorsOccurred + ", createdBy=" + createdBy + ", userAgent=" + userAgent); +// // Code any necessary validation here. +// +// response.put("detailedResponse", "UpdateSessionLog API: Session log record validated successfully."); +// response.put("success", true); +// return response; +// } +// } + + + +// // This function is muted until needed. +// @RequiresLogin +// public static class UpdateUserAnimalAbstractPreferencesAction extends MutatingApiAction +// { +// @Override +// public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception +// { +// // Creates variables. +// ApiSimpleResponse response = new ApiSimpleResponse(); +//// NotificationToolkit notificationToolkit = new NotificationToolkit(); +// +// // Retrieves passed-in arguments. +// JSONObject myForm = form.getJsonObject(); +// int targetUser = myForm.getInt("target_user"); +// String insertOrUpdate = myForm.getString("insert_or_update").toString(); +// +// // Validates that the preferences to change are supported. +// String[] supportedPreferences = { +// "show_id", +// "show_gender", +// "show_availability", +// "show_room", +// "show_cage", +// "show_condition", +// "show_num_animals_in_cage", +// "show_status", +// "show_age", +// "show_birth", +// "show_dam", +// "show_sire", +// "show_tb_date", +// "show_prepaid", +// "show_mgap_ids", +// "show_most_recent_weight", +// "show_most_recent_weight_date", +// "show_hold", +// "show_medical", +// "show_current_behaviors", +// "show_most_recent_alopecia_score", +// "show_most_recent_body_condition_score", +// "show_origin", +// "show_geographic_origin", +// "show_ancestry", +// "show_most_recent_arrival", +// "show_most_recent_departure", +// "show_death", +// "show_remark", +// "show_mgap_sequence_types" +// }; +// List supportedPreferencesList = Arrays.asList(supportedPreferences); +// Iterator keys = myForm.keys(); +// Boolean hasUnsupportedPreferences = false; +// while (keys.hasNext()) { +// String key = keys.next(); +// if (!key.equals("target_user") && !key.equals("date_last_updated") && !key.equals("insert_or_update")) { +// if (!supportedPreferencesList.contains(key)) { +// hasUnsupportedPreferences = true; +// } +// } +// } +// if (hasUnsupportedPreferences) { +// response.put("detailedResponse", "UpdateUserAnimalAbstractPreferences API: Abstract preferences were unsupported."); +// response.put("success", false); +// return response; +// } +// +// // Verifies user is either inserting (with no previous preferences) or updating (with previous preferences). +// SimpleFilter animalAbstractPreferencesFilter = new SimpleFilter("target_user", targetUser, CompareType.EQUAL); +// TableInfo ti = QueryService.get().getUserSchema(getUser(), getContainer(), "wnprc_ios_app").getTable("user_animal_abstract_preferences"); +// TableSelector myTable = new TableSelector(ti, PageFlowUtil.set("target_user"), animalAbstractPreferencesFilter, null); +// Map[] rows = myTable.getMapArray(); +// if (insertOrUpdate.equals("insert")) { +// if (rows.length > 0) { +// response.put("detailedResponse", "UpdateUserAnimalAbstractPreferences API: Insert failed due to multiple rows existing."); +// response.put("success", false); +// return response; +// } +// } +// else if (insertOrUpdate.equals("update")) { +// if (rows.length != 1) { +// response.put("detailedResponse", "UpdateUserAnimalAbstractPreferences API: Update failed due to not having 1 existing row."); +// response.put("success", false); +// return response; +// } +// } +// +// // Successfully completes validation. +// _log.info("UpdateUserAnimalAbstractPreferencesAction API: targetUser=" + targetUser + ", insertOrUpdate=" + insertOrUpdate); +// response.put("detailedResponse", "UpdateUserAnimalAbstractPreferences API: Abstract preferences validated successfully."); +// response.put("success", true); +// return response; +// } +// } +} diff --git a/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appManager.java b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appManager.java new file mode 100644 index 000000000..14c3610e5 --- /dev/null +++ b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appManager.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 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.wnprc_ios_app; + +public class wnprc_ios_appManager +{ + private static final wnprc_ios_appManager _instance = new wnprc_ios_appManager(); + + private wnprc_ios_appManager() + { + // prevent external construction with a private default constructor + } + + public static wnprc_ios_appManager get() + { + return _instance; + } +} \ No newline at end of file diff --git a/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appModule.java b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appModule.java new file mode 100644 index 000000000..3025a5961 --- /dev/null +++ b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appModule.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2025 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.wnprc_ios_app; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbSchemaType; +//import org.labkey.api.ehr.EHRService; +import org.labkey.api.module.DefaultModule; +import org.labkey.api.module.Module; +import org.labkey.api.module.ModuleContext; +import org.labkey.api.module.SimpleModule; +import org.labkey.api.module.SpringModule; +import org.labkey.api.query.DefaultSchema; +import org.labkey.api.query.QuerySchema; +import org.labkey.api.query.QueryService; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.WebPartFactory; +//import org.labkey.wnprc_ehr.updates.ModuleUpdate; + +import javax.swing.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +public class wnprc_ios_appModule extends DefaultModule +{ + public static final String NAME = "wnprc_ios_app"; + + @Override + public String getName() + { + return NAME; + } + + @Override + public @Nullable Double getSchemaVersion() + { + return 25.001; + } + + @Override + public boolean hasScripts() + { + return true; + } + + @Override + @NotNull + protected Collection createWebPartFactories() + { + return Collections.emptyList(); + } + + @Override + protected void init() + { + addController(wnprc_ios_appController.NAME, wnprc_ios_appController.class); + } + + @Override + public void doStartup(ModuleContext moduleContext) + { + // add a container listener so we'll know when our container is deleted: + ContainerManager.addContainerListener(new wnprc_ios_appContainerListener()); + +// ModuleUpdate.onStartup(moduleContext, this); +// EHRService.get().registerTableCustomizer(this, org.labkey.wnprc_ehr.table.WNPRC_EHRCustomizer.class); + + DefaultSchema.registerProvider(wnprc_ios_appSchema.NAME, new DefaultSchema.SchemaProvider(this) + { + @Override + public @Nullable QuerySchema createSchema(DefaultSchema schema, Module module) + { + DbSchema dbSchema = DbSchema.get(wnprc_ios_appSchema.NAME, DbSchemaType.Module); + return QueryService.get().createSimpleUserSchema(dbSchema.getQuerySchemaName(), null, schema.getUser(), schema.getContainer(), dbSchema); + } + }); + } + + @Override + @NotNull + public Collection getSummary(Container c) + { + return Collections.emptyList(); + } + + @Override + @NotNull + public Set getSchemaNames() + { + return Collections.singleton(wnprc_ios_appSchema.NAME); + } + +} \ No newline at end of file diff --git a/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appSchema.java b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appSchema.java new file mode 100644 index 000000000..e5e636877 --- /dev/null +++ b/wnprc_ios_app/src/org/labkey/wnprc_ios_app/wnprc_ios_appSchema.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 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.wnprc_ios_app; + +import org.labkey.api.data.DbSchema; +import org.labkey.api.data.DbSchemaType; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.dialect.SqlDialect; + +public class wnprc_ios_appSchema +{ + private static final wnprc_ios_appSchema _instance = new wnprc_ios_appSchema(); + // Schema name. + public static final String NAME = "wnprc_ios_app"; + // Table names to expose via schema browser. + public static final String PUSH_NOTIFICATIONS_TABLE_NAME = "push_notifications"; + public static final String SESSION_LOG_TABLE_NAME = "session_log"; + public static final String REPORTED_ISSUES_TABLE_NAME = "reported_issues"; + public static final String USER_ANIMAL_ABSTRACT_PREFERENCES_TABLE_NAME = "user_animal_abstract_preferences"; + + + + public static wnprc_ios_appSchema getInstance() + { + return _instance; + } + + private wnprc_ios_appSchema() + { + // private constructor to prevent instantiation from + // outside this class: this singleton should only be + // accessed via org.labkey.wnprc_ios_app.wnprc_ios_appSchema.getInstance() + } + + public DbSchema getSchema() + { + return DbSchema.get(NAME, DbSchemaType.Module); + } + + public SqlDialect getSqlDialect() + { + return getSchema().getSqlDialect(); + } + + public TableInfo getPushNotificationsTable() { + return getSchema().getTable(PUSH_NOTIFICATIONS_TABLE_NAME); + } + + public TableInfo getSessionLogTable() { + return getSchema().getTable(SESSION_LOG_TABLE_NAME); + } + + public TableInfo getReportedIssuesTable() { + return getSchema().getTable(REPORTED_ISSUES_TABLE_NAME); + } + + public TableInfo getUserAnimalAbstractPreferencesTableName() { + return getSchema().getTable(USER_ANIMAL_ABSTRACT_PREFERENCES_TABLE_NAME); + } +} From 004a48b1108a6f64d7ba981f43db439b12c7ce85 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Wed, 5 Aug 2026 16:18:42 -0500 Subject: [PATCH 11/12] Removed placeholders README and begin.html. Removed gitignore file. Added header to build.gradle and module.properties. Updated upgrade script to 26. --- wnprc_ios_app/.gitignore | 1 - wnprc_ios_app/README.md | 0 wnprc_ios_app/build.gradle | 16 ++++++++++++++++ wnprc_ios_app/module.properties | 16 ++++++++++++++++ ...25.000.sql => wnprc_ios_app-0.000-26.000.sql} | 0 wnprc_ios_app/resources/views/begin.html | 1 - 6 files changed, 32 insertions(+), 2 deletions(-) delete mode 100644 wnprc_ios_app/.gitignore delete mode 100644 wnprc_ios_app/README.md rename wnprc_ios_app/resources/schemas/dbscripts/postgresql/{wnprc_ios_app-0.000-25.000.sql => wnprc_ios_app-0.000-26.000.sql} (100%) delete mode 100644 wnprc_ios_app/resources/views/begin.html diff --git a/wnprc_ios_app/.gitignore b/wnprc_ios_app/.gitignore deleted file mode 100644 index e43b0f988..000000000 --- a/wnprc_ios_app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.DS_Store diff --git a/wnprc_ios_app/README.md b/wnprc_ios_app/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/wnprc_ios_app/build.gradle b/wnprc_ios_app/build.gradle index 07df36599..982038bdd 100644 --- a/wnprc_ios_app/build.gradle +++ b/wnprc_ios_app/build.gradle @@ -1,3 +1,19 @@ +// /* +// * Copyright (c) 2025 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. +// */ + import org.labkey.gradle.util.BuildUtils plugins { diff --git a/wnprc_ios_app/module.properties b/wnprc_ios_app/module.properties index 0b85a7ee0..fcab70df1 100644 --- a/wnprc_ios_app/module.properties +++ b/wnprc_ios_app/module.properties @@ -1,3 +1,19 @@ +# /* +# * Copyright (c) 2025 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. +# */ + ModuleClass: org.labkey.wnprc_ios_app.wnprc_ios_appModule Name: wnprc_ios_app ManageVersion: false diff --git a/wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-25.000.sql b/wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-26.000.sql similarity index 100% rename from wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-25.000.sql rename to wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-26.000.sql diff --git a/wnprc_ios_app/resources/views/begin.html b/wnprc_ios_app/resources/views/begin.html deleted file mode 100644 index 16f2c823f..000000000 --- a/wnprc_ios_app/resources/views/begin.html +++ /dev/null @@ -1 +0,0 @@ -

PrimatePal admin page will be shown here.

\ No newline at end of file From 14fe7c69b71b3125d3d24037e1bde1ecf66e1fc0 Mon Sep 17 00:00:00 2001 From: aschmidt34 Date: Wed, 5 Aug 2026 16:33:00 -0500 Subject: [PATCH 12/12] Re-added README (with some text now) and module landing page (so it doesn't throw errors when navigating here). --- wnprc_ios_app/README.md | 1 + wnprc_ios_app/resources/views/begin.html | 1 + 2 files changed, 2 insertions(+) create mode 100644 wnprc_ios_app/README.md create mode 100644 wnprc_ios_app/resources/views/begin.html diff --git a/wnprc_ios_app/README.md b/wnprc_ios_app/README.md new file mode 100644 index 000000000..2beeedacd --- /dev/null +++ b/wnprc_ios_app/README.md @@ -0,0 +1 @@ +This is a module that communicates with the PrimatePal iOS app. \ No newline at end of file diff --git a/wnprc_ios_app/resources/views/begin.html b/wnprc_ios_app/resources/views/begin.html new file mode 100644 index 000000000..16f2c823f --- /dev/null +++ b/wnprc_ios_app/resources/views/begin.html @@ -0,0 +1 @@ +

PrimatePal admin page will be shown here.

\ No newline at end of file