From 97368b1964d7a5745fa2443f79b0dce026500d62 Mon Sep 17 00:00:00 2001
From: aschmidt34 <124093649+aschmidt34@users.noreply.github.com>
Date: Wed, 5 Aug 2026 16:44:05 -0500
Subject: [PATCH 1/3] 26.3 fb ios app integration (#1002)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
PR DETAILS:
- anesthesiaRecoveriesFullHistory.sql: Defines sql for table that holds
data for full history.
- anesthesiaRecovery.query.xml: Defines row details for
anesthesiaRecovery official table.
- anesthesiaRecovery/.qview.xml: Defines anesthesiaRecovery official
table default view.
- anesthesiaRecovery/Full History.qview.xml: Defines anesthesiaRecovery
full history table default view.
- anesthesiaRecovery/.qview.xml: Defines anesthesiaRecovery official
table summary view.
- anesthesiaRecovery/.qview.xml: Defines anesthesiaRecovery official
table full history view.
- anesthesiaRecovery/Full History.qview.xml: Adds anesthesia recovery as
an option for full history.
- session_log.query.xml: Adds a customizer for the session log query.
- WNPRC_EHRController.java: Created trigger to validate & upload data to
anesthesiaRecovery dataset.
- WNPRC_EHRModule: Registers email notification.
- AnesthesiaRecoveryReviewNotification.java: Creates email notification
to let users know if there are unfinished recoveries or rows marked as
'review required'.
- NotificationToolkit.java: Updated functionality to allow multiple
params for field clauses.
- WNPRC_EHRCustomizer.java: Added customizers for anesthesiaRecovery and
sessionLog tables.
- .gitignore: Added .DS_Store so iOS only uploads necessary files and no
caches.
- README: Empty, but created for future use.
- build.gradle: Needed to create new external iOS module.
- module.properties: Needed to create new external iOS module.
- wnprc_ios_app-0.000-25.000.sql: Upgrade script that inserts iOS app
tables into app on first load.
- wnprc_ios_app.xml: Defines default .xml view for all iOS app tables.
- begin.html: Created a default landing page for new iOS module.
- wnprc_ios_appContainerListener.java: Needed to create new external iOS
module.
- wnprc_ios_appController.java: Creates all validation functions for new
iOS module tables. They are muted for now and can be unmuted when
needed.
- wnprc_ios_appManager.java: Needed to create new external iOS module.
- wnprc_ios_appModule.java: Needed to create new external iOS module.
- wnprc_ios_appSchema.java: Needed to create new external iOS module.
## Rationale
## Related Pull Requests
-
## Changes
-
---
.../study/anesthesiaRecoveriesFullHistory.sql | 12 +
.../study/anesthesiaRecovery.query.xml | 75 ++++
.../study/anesthesiaRecovery/.qview.xml | 37 ++
.../anesthesiaRecovery/Full History.qview.xml | 28 ++
.../anesthesiaRecovery/Summary.qview.xml | 12 +
.../queries/study/wnprcFullHistory.sql | 21 +-
.../queries/study/wnprcFullHistory/.qview.xml | 2 +-
.../wnprc_ios_app/session_log.query.xml | 10 +
.../labkey/wnprc_ehr/WNPRC_EHRController.java | 339 ++++++++++++++++++
.../org/labkey/wnprc_ehr/WNPRC_EHRModule.java | 1 +
.../AnesthesiaRecoveryReviewNotification.java | 204 +++++++++++
.../notification/NotificationToolkit.java | 26 +-
.../wnprc_ehr/table/WNPRC_EHRCustomizer.java | 197 +++++++++-
wnprc_ios_app/README.md | 1 +
wnprc_ios_app/build.gradle | 21 ++
wnprc_ios_app/module.properties | 22 ++
.../postgresql/wnprc_ios_app-0.000-26.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 ++++
24 files changed, 1753 insertions(+), 13 deletions(-)
create mode 100644 WNPRC_EHR/resources/queries/study/anesthesiaRecoveriesFullHistory.sql
create mode 100644 WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml
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
create mode 100644 WNPRC_EHR/resources/queries/wnprc_ios_app/session_log.query.xml
create mode 100644 WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java
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-26.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/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
new file mode 100644
index 000000000..3b2684fbd
--- /dev/null
+++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery.query.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+ true
+ false
+
+
+
+ true
+ false
+
+
+
+ Recovery Start Time Final
+
+
+
+ Task Id
+
+ ehr
+ tasks
+ taskid
+
+ /ehr/WNPRC/EHR/taskDetails.view?formtype=Anesthesia%20Recovery&taskid=${taskid}
+ true
+ false
+
+
+
+ true
+ false
+
+
+
+ true
+ false
+
+
+
+ true
+ false
+
+
+
+ Recovery Reason Final
+
+
+
+ Group ID Final
+
+
+
+ Cage Final
+
+
+
+ Location Final
+
+
+
+ Room Final
+
+
+
+ Status
+
+
+
+
+
+
+
\ No newline at end of file
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..971cfb1b3
--- /dev/null
+++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/.qview.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..7d6d023e9
--- /dev/null
+++ b/WNPRC_EHR/resources/queries/study/anesthesiaRecovery/Summary.qview.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
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/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 c6f6e98f2..0c215b13e 100644
--- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java
+++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRController.java
@@ -18,13 +18,16 @@
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;
+import org.joda.time.LocalDateTime;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.Jsoup;
@@ -51,12 +54,16 @@
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;
+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;
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;
@@ -64,6 +71,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;
@@ -116,6 +124,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;
@@ -130,7 +139,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;
@@ -141,6 +152,10 @@
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;
import static java.time.temporal.ChronoUnit.DAYS;
@@ -2452,4 +2467,328 @@ public Object execute(NecropsyEditRequestNotificationForm form, BindException er
}
}
+ @RequiresLogin
+ public static class UpdateAnesthesiaRecoveryDatasetAction extends MutatingApiAction {
+
+ @Override
+ public Object execute(SimpleApiJsonForm form, BindException errors) throws Exception {
+ _log.info("UPDATE CALLED: UpdateAnesthesiaRecoveryDatasetAction()");
+
+ // 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.
+ // 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
", 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));
+ }
+ }
+
+ // 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;
+ }
+ }
+
+ 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/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 "";
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..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
@@ -101,9 +101,14 @@ else if (table.getName().equalsIgnoreCase("breeding_encounters") && table.getSch
} else if (matches(table, "wnprc", "animal_requests")) {
customizeAnimalRequestsTable((AbstractTableInfo) table);
}
+ 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");
-
}
}
@@ -315,6 +320,196 @@ private void customizeFeedingTable(AbstractTableInfo ti)
}
+ private void customizeAnesthesiaRecoveryTable(AbstractTableInfo ti) {
+ // 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).";
+ // Creates SQL script to define what to show in column.
+ 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);
+ 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) {
+ // 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");
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/build.gradle b/wnprc_ios_app/build.gradle
new file mode 100644
index 000000000..982038bdd
--- /dev/null
+++ b/wnprc_ios_app/build.gradle
@@ -0,0 +1,21 @@
+// /*
+// * 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 {
+ 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..fcab70df1
--- /dev/null
+++ b/wnprc_ios_app/module.properties
@@ -0,0 +1,22 @@
+# /*
+# * 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
+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-26.000.sql b/wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-26.000.sql
new file mode 100644
index 000000000..cd1a40d1a
--- /dev/null
+++ b/wnprc_ios_app/resources/schemas/dbscripts/postgresql/wnprc_ios_app-0.000-26.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 76332f89145d9b1f914cbf65cf4004aacb3919a3 Mon Sep 17 00:00:00 2001
From: aschmidt34 <124093649+aschmidt34@users.noreply.github.com>
Date: Thu, 13 Aug 2026 16:07:55 -0500
Subject: [PATCH 2/3] 26.3 fb ios app production merge fix (#1009)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Updated 4 files to fix the errors being thrown by TeamCity build. My
anesthesiaRecovery dataset will be created manually after deployment, so
these files needed to be updated (per Marty) to ensure the automated
tests know what the dataset will look like.
- datasets_manifest.xml: Added my dataset to the list (fixes TeamCity
error: WNPRC_EHRTest)
- datasets_metadata.xml: Listed all the columns in my dataset (fixes
TeamCity error: WNPRC_EHRTest)
- wnprcEhrTestStudyPolicy.xml: Added my table permissions (fixes
TeamCity error: testMprDataEntry)
- wnprc_ios_appModule.java: Updated my schema after changing my upgrade
script to 0.000-26.000 (fixes TeamCity error: SchemaXMLTestCase)
## Rationale
## Related Pull Requests
-
## Changes
-
---
.../study/datasets/datasets_manifest.xml | 3 +
.../study/datasets/datasets_metadata.xml | 113 ++++++++++++++++++
.../wnprc_ehr/wnprcEhrTestStudyPolicy.xml | 6 +
.../wnprc_ios_app/wnprc_ios_appModule.java | 2 +-
4 files changed, 123 insertions(+), 1 deletion(-)
diff --git a/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_manifest.xml b/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_manifest.xml
index af54a8e9d..06e96046e 100644
--- a/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_manifest.xml
+++ b/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_manifest.xml
@@ -232,5 +232,8 @@
+
+
+
\ No newline at end of file
diff --git a/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_metadata.xml b/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_metadata.xml
index 82b520229..e1680dd5d 100644
--- a/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_metadata.xml
+++ b/WNPRC_EHR/resources/referenceStudy/study/datasets/datasets_metadata.xml
@@ -16334,4 +16334,117 @@
+
+
+
+ varchar
+ Id
+ Subject identifier
+ http://cpas.labkey.com/Study#ParticipantId
+ http://cpas.labkey.com/Study#ParticipantId
+ false
+ /ehr/participantView.view?participantId=${id}
+
+ ptid
+ participantid
+
+
+ study
+ Animal
+ Id
+
+ ALWAYS_OFF
+ 32
+
+
+ integer
+
+
+ timestamp
+
+
+ timestamp
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+ varchar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WNPRC_EHR/test/sampledata/wnprc_ehr/wnprcEhrTestStudyPolicy.xml b/WNPRC_EHR/test/sampledata/wnprc_ehr/wnprcEhrTestStudyPolicy.xml
index 69867afde..b91d36ea9 100644
--- a/WNPRC_EHR/test/sampledata/wnprc_ehr/wnprcEhrTestStudyPolicy.xml
+++ b/WNPRC_EHR/test/sampledata/wnprc_ehr/wnprcEhrTestStudyPolicy.xml
@@ -443,5 +443,11 @@
+
+
+
+
+
+
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
index 3025a5961..a8e555886 100644
--- 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
@@ -53,7 +53,7 @@ public String getName()
@Override
public @Nullable Double getSchemaVersion()
{
- return 25.001;
+ return 26.001;
}
@Override
From 2b7e2e83b9cfad2b871a77db5d0564b23b066597 Mon Sep 17 00:00:00 2001
From: Marty Pradere
Date: Fri, 14 Aug 2026 09:51:35 -0700
Subject: [PATCH 3/3] Handle missing file history when listing virology results
to import (#1008)
## Rationale
The "Import Results from File" list in the virology results grid could
fail to render. Each candidate file's upload date comes from a webdav
file-history lookup, and two paths through that lookup were broken: a
file with no file system audit record threw on `history[0].data.date`,
and the lookup's own failure callback called an undefined `reject`,
throwing a ReferenceError instead of rejecting. Either one rejected the
enclosing `Promise.all`, which had no rejection handler, so the window
stayed behind a "Loading..." mask with no error shown.
## Changes
- `getFileHistory` resolves instead of rejecting on a failed lookup, so
one unreadable file no longer drops the whole import list.
- Guarded the empty-history case; those files report their upload date
as `Unknown` rather than a substituted timestamp.
- Added a rejection handler to the `Promise.all` so failures hide the
loading mask and surface an alert.
- `getFileHistory` resolves `{name, uploaded}` rather than a positional
`[filename, history]` pair.
---
.../resources/web/ehr/ext3/ehrGridFormPanel.js | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/WNPRC_EHR/resources/web/ehr/ext3/ehrGridFormPanel.js b/WNPRC_EHR/resources/web/ehr/ext3/ehrGridFormPanel.js
index 34c7899ab..829869620 100644
--- a/WNPRC_EHR/resources/web/ehr/ext3/ehrGridFormPanel.js
+++ b/WNPRC_EHR/resources/web/ehr/ext3/ehrGridFormPanel.js
@@ -1077,8 +1077,8 @@ EHR.ext.GridFormPanel = Ext.extend(Ext.Panel,
if (extension === 'xlsx' || extension === 'xls') {
// need to get the date it was uploaded,
// since 'record' only provides the date when the actual file was created
- promises.push(getFileHistory(virologyResultsFolder, record.data.name).then((history) => {
- files.push({"name": history[0], "uploaded": history[1][0].data.date})
+ promises.push(getFileHistory(virologyResultsFolder, record.data.name).then((file) => {
+ files.push(file)
}))
}
@@ -1113,6 +1113,10 @@ EHR.ext.GridFormPanel = Ext.extend(Ext.Panel,
importFromFileWindow.removeAll();
importFromFileWindow.add(selectFilePanel);
importFromFileWindow.doLayout();
+ }).catch((e) => {
+ Ext.Msg.hide();
+ console.error(e);
+ Ext.Msg.alert('Error', 'Unable to build the list of files to import.');
})
},
@@ -1130,14 +1134,17 @@ EHR.ext.GridFormPanel = Ext.extend(Ext.Panel,
}
});
function getFileHistory(fileSystem, filename) {
+ // Always resolves so one unreadable file still leaves the rest importable.
return new Promise(resolve => {
fileSystem.getHistory({
path: '/' + filename,
success: function(fileSystem,path,history) {
- resolve([filename,history]);
+ // A file placed on the server outside of an upload has no audit record.
+ resolve({name: filename, uploaded: history && history.length ? history[0].data.date : null});
},
failure: function(f) {
- reject(f);
+ console.error('Unable to read file history for ' + filename, f);
+ resolve({name: filename, uploaded: null});
}
})
});
@@ -1307,7 +1314,7 @@ EHR.ext.GridFormPanel = Ext.extend(Ext.Panel,
html: '' + file.name + ''
},
{
- html: '' + new Date(file.uploaded).format("Y-m-d H:i")+ ''
+ html: '' + (file.uploaded ? new Date(file.uploaded).format("Y-m-d H:i") : 'Unknown') + ''
},
];