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] 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);
+ }
+}