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 e1957c577..18fdc6f4a 100644 --- a/WNPRC_EHR/resources/queries/study/wnprcFullHistory.sql +++ b/WNPRC_EHR/resources/queries/study/wnprcFullHistory.sql @@ -53,4 +53,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 8b0d6ab21..636574276 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> rowsToValidate = new ArrayList<>(); + JSONArray rowsArray = inputJson.getJSONArray("rows"); + for (int i = 0; i < rowsArray.length(); i++) { + rowsToValidate.add(rowsArray.getJSONObject(i).toMap()); + } + + + // 3. Creates the object that collects errors from all failing rows, or the successful rows and tasks to upload. + BatchValidationException batchErrors = new BatchValidationException(); + List> rowsToInsert = new ArrayList<>(); + List> rowsToDelete = new ArrayList<>(); + List> tasksToInsert = new ArrayList<>(); + List> tasksToUpdate = new ArrayList<>(); + + + // 4. Loops through each row and validates them. + for (int i = 0; i < rowsToValidate.size(); i++) { + // Retrieves the current row. + Map row = rowsToValidate.get(i); + // Retrieves required values. + String id = row.get("Id") != null ? row.get("Id").toString() : null; + String observer = row.get("observer") != null ? row.get("observer").toString() : null; + String recoveryId = row.get("recoveryId") != null ? row.get("recoveryId").toString() : null; + String observation = row.get("observation") != null ? row.get("observation").toString() : null; + String submitterInitials = row.get("submitterInitials") != null ? row.get("submitterInitials").toString() : null; + java.time.LocalDateTime serverDate = java.time.LocalDateTime.now(); // Explicitly import Java here, otherwise script defaults to joda time due to both being imported above. + // TODO: Ask users if initials should be required for deleting a row. + if (submitterInitials == null || submitterInitials == "") { + submitterInitials = "empty"; + } + // Retrieves optional values (for all observations). + String observerComments = row.get("observerComments") != null ? row.get("observerComments").toString() : null; + String assignedTo = row.get("assignedTo") != null ? row.get("assignedTo").toString() : null; + String recoverySpeed = row.get("recoverySpeed") != null ? row.get("recoverySpeed").toString() : null; + String recoveryCondition = row.get("recoveryCondition") != null ? row.get("recoveryCondition").toString() : null; + String finalizeComments = row.get("finalizeComments") != null ? row.get("finalizeComments").toString() : null; + String cageLockSecure = row.get("cageLockSecure") != null ? row.get("cageLockSecure").toString() : null; + String deviceId = row.get("deviceId") != null ? row.get("deviceId").toString() : null; + // Retrieves optional values (for fields that should only be updated on 'Imported' observations - customizer sets all other observation rows to reference 'Imported' row for these values). + String recoveryReason = null; + String groupId = null; + String cage = null; + String location = null; + String room = null; + if (observation.equals("Imported")) { + recoveryReason = row.get("recoveryReason") != null ? row.get("recoveryReason").toString() : "none"; + groupId = row.get("groupId") != null ? row.get("groupId").toString() : null; + cage = row.get("cage") != null ? row.get("cage").toString() : null; + location = row.get("location") != null ? row.get("location").toString() : null; + room = row.get("room") != null ? row.get("room").toString() : null; + } + // Gets passed-in review required status. + String reviewRequired = row.get("reviewRequired") != null ? row.get("reviewRequired").toString() : null; + Boolean reviewRequiredParsed = reviewRequired != null ? Boolean.parseBoolean(reviewRequired) : false; + Integer rowQcState = reviewRequiredParsed == true ? EHRService.QCSTATES.ReviewRequired.getQCState(getContainer()).getRowId() : EHRService.QCSTATES.Completed.getQCState(getContainer()).getRowId(); + // Sets data to new row variable. + Map validatedRow = new HashMap<>(); + validatedRow.put("Id", id); + validatedRow.put("date", Timestamp.valueOf(serverDate)); + validatedRow.put("recoveryReason", recoveryReason); + validatedRow.put("observer", observer); + validatedRow.put("recoveryId", recoveryId); + validatedRow.put("observation", observation); + validatedRow.put("submitterInitials", submitterInitials); + validatedRow.put("observerComments", observerComments); + validatedRow.put("room", room); + validatedRow.put("assignedTo", assignedTo); + validatedRow.put("recoverySpeed", recoverySpeed); + validatedRow.put("recoveryCondition", recoveryCondition); + validatedRow.put("groupId", groupId); + validatedRow.put("finalizeComments", finalizeComments); + validatedRow.put("location", location); + validatedRow.put("cage", cage); + validatedRow.put("cageLockSecure", cageLockSecure); + validatedRow.put("deviceId", deviceId); + validatedRow.put("QCState", rowQcState); + + // 4a. Check row for required fields. + String[] requiredFields = { + "Id", "observer", "recoveryId", "observation", "submitterInitials" + }; + boolean missingField = false; + for (String field : requiredFields) { + if (validatedRow.get(field) == null || validatedRow.get(field).toString().trim().isEmpty()) { + batchErrors.addRowError(new ValidationException("Row " + (i + 1) + " is missing required field: " + field)); + missingField = true; + break; // Stop checking this row, move to next. + } + } + if (missingField) { + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + + // 4b. Validate existing table data before updating (depending on observation being added). + if (observation.equals("Imported")) { + // Verify no other active recoveries exist for the current animal. + String[] existingRecoveriesTargetColumn = new String[]{"recoveryId"}; + SimpleFilter recoveriesStartedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesStartedFilter.addCondition("observation", "Imported", CompareType.EQUAL); + SimpleFilter recoveriesFinishedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesFinishedFilter.addCondition("observation", "Fully Recovered;Deleted", CompareType.IN); + ArrayList> recoveriesStartedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesStartedFilter, null, existingRecoveriesTargetColumn); + ArrayList> recoveriesFinishedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesFinishedFilter, null, existingRecoveriesTargetColumn); + // Verify counts match. + if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows != null && !recoveriesFinishedRows.isEmpty() && recoveriesStartedRows.size() != recoveriesFinishedRows.size()) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before beginning a new one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows == null) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before beginning a new one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + // Verifies animal is currently alive and exists at center. + SimpleFilter existsAliveAtCenterFilter = new SimpleFilter("id", id, CompareType.EQUAL); + String[] existsAliveAtCenterTargetColumns = new String[]{"Id", "calculated_status"}; + ArrayList> existsAliveAtCenterRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "demographics", existsAliveAtCenterFilter, null, existsAliveAtCenterTargetColumns); + if (existsAliveAtCenterRows == null || existsAliveAtCenterRows.isEmpty()) { + batchErrors.addRowError((new ValidationException("Animal does not currently exist at the center."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else if (!existsAliveAtCenterRows.get(0).get("calculated_status").equals("Alive")) { + batchErrors.addRowError((new ValidationException("Animal is no longer alive."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + } + else if (observation.equals("Deleted")) { + // Verify only 1 row exists with this 'recoveryId' and has the status of 'Imported', then retrieve the lsid. + String[] existingRecoveryLsidColumn = new String[]{"lsid"}; + SimpleFilter existingRecoveryFilter = new SimpleFilter("recoveryId", recoveryId, CompareType.EQUAL); + ArrayList> existingRecoveryRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", existingRecoveryFilter, null, existingRecoveryLsidColumn); + if (existingRecoveryRows == null || existingRecoveryRows.isEmpty()) { + batchErrors.addRowError((new ValidationException("Recovery: " + recoveryId + " has no data to delete."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + if (existingRecoveryRows.size() > 1) { + batchErrors.addRowError((new ValidationException("Recovery: " + recoveryId + " has already had 1 or more observations after 'Imported'. In-progress recoveries cannot be deleted."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else { + validatedRow.put("lsid", existingRecoveryRows.get(0).get("lsid")); + } + } + else if (observation.equals("Unfinalized")) { + // Verify this animal has no other open recoveries currently active. + String[] existingRecoveriesTargetColumn = new String[]{"recoveryId"}; + SimpleFilter recoveriesStartedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesStartedFilter.addCondition("observation", "Imported", CompareType.EQUAL); + SimpleFilter recoveriesFinishedFilter = new SimpleFilter("id", id, CompareType.EQUAL); + recoveriesFinishedFilter.addCondition("observation", "Fully Recovered;Deleted", CompareType.IN); + ArrayList> recoveriesStartedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesStartedFilter, null, existingRecoveriesTargetColumn); + ArrayList> recoveriesFinishedRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", recoveriesFinishedFilter, null, existingRecoveriesTargetColumn); + // Verify counts match. + if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows != null && !recoveriesFinishedRows.isEmpty() && recoveriesStartedRows.size() != recoveriesFinishedRows.size()) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before restoring a previous one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else if (recoveriesStartedRows != null && !recoveriesStartedRows.isEmpty() && recoveriesFinishedRows == null) { + batchErrors.addRowError((new ValidationException("Animal: " + id + " already has an active recovery in-progress. Please finish this recovery before restoring a previous one."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + + // Verify a row exists with this 'recoveryId' and a status of 'Fully Recovered', then retrieve the lsid. + String[] existingRecoveryLsidColumn = new String[]{"lsid"}; + SimpleFilter existingFinalizedRecoveryFilter = new SimpleFilter("recoveryId", recoveryId, CompareType.EQUAL); + existingFinalizedRecoveryFilter.addCondition("observation", "Fully Recovered", CompareType.EQUAL); + ArrayList> existingRecoveryRows = notificationToolkit.getTableMultiRowMultiColumnWithFieldKeys(getContainer(), getUser(), "study", "anesthesiaRecovery", existingFinalizedRecoveryFilter, null, existingRecoveryLsidColumn); + if (existingRecoveryRows == null || existingRecoveryRows.isEmpty()) { + batchErrors.addRowError((new ValidationException("Recovery: " + recoveryId + " has no recovery data to resume."))); + continue; // Invalid row; skip adding to batch update and continue checking other rows. + } + else { + validatedRow.put("lsid", existingRecoveryRows.get(0).get("lsid")); + } + } + + // 4c. Create or Update task into task dataset. + Map taskRecord = new HashMap<>(); + taskRecord.put("taskid", recoveryId); + taskRecord.put("title", "Anesthesia Recovery"); + taskRecord.put("category", "task"); + taskRecord.put("formType", "Anesthesia Recovery"); + taskRecord.put("assignedTo", getUser().getUserId()); + if (observation.equals("Imported")) { + taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); + tasksToInsert.add(taskRecord); + } + else if (observation.equals("Sitting Upright") || observation.equals("Laying Down")) { +// taskRecord.put("qcstate", qcStateStarted); + taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); + tasksToUpdate.add(taskRecord); + } + else if (observation.equals("Fully Recovered")) { + taskRecord.put("qcstate", EHRService.QCSTATES.Completed.getQCState(getContainer()).getRowId()); + tasksToUpdate.add(taskRecord); + } + else if (observation.equals("Deleted")) { + taskRecord.put("qcstate", EHRService.QCSTATES.DeleteRequested.getQCState(getContainer()).getRowId()); + tasksToUpdate.add(taskRecord); + } + else if (observation.equals("Unfinalized")) { + taskRecord.put("qcstate", EHRService.QCSTATES.Scheduled.getQCState(getContainer()).getRowId()); + tasksToUpdate.add(taskRecord); + } + + // 4e. Current row is valid, added to rowsToInsert for the batch update. + if (observation.equals("Unfinalized") || observation.equals("Deleted")) { + rowsToDelete.add(validatedRow); + } else { + rowsToInsert.add(validatedRow); + } + } + + // 6. Report all manual validation errors back to the user. + if (batchErrors.hasErrors()) { + String combinedErrors = batchErrors.getRowErrors().stream() + .map(error -> "Row " + error.getRowNumber() + ": " + error.getMessage()) + .collect(Collectors.joining("\n")); + response.put("success", false); + response.put("detailedResponse", combinedErrors); + response.put("errors", batchErrors.getRowErrors()); + return response; + } + + // 7. Officially updates the database if all manual validation has been passed. + try (DbScope.Transaction transaction = StudySchema.getInstance().getSchema().getScope().ensureTransaction()) { + // 7a. Creates the environment variables. + TableInfo anesthesiaTableInfo = QueryService.get().getUserSchema(getUser(), getContainer(), "study").getTable("anesthesiaRecovery"); + TableInfo tasksTableInfo = QueryService.get().getUserSchema(getUser(), getContainer(), "ehr").getTable("tasks"); + QueryUpdateService anesthesiaTableService = anesthesiaTableInfo.getUpdateService(); + QueryUpdateService tasksTableService = tasksTableInfo.getUpdateService(); + BatchValidationException dbErrors = new BatchValidationException(); + rowsToInsert = SimpleQueryUpdater.makeRowListCaseInsensitive(rowsToInsert); + rowsToDelete = SimpleQueryUpdater.makeRowListCaseInsensitive(rowsToDelete); + tasksToInsert = SimpleQueryUpdater.makeRowListCaseInsensitive(tasksToInsert); + tasksToUpdate = SimpleQueryUpdater.makeRowListCaseInsensitive(tasksToUpdate); + + // 7b. Creates the tasks containing all rows to be inserted. + List> anesthesiaRowsToInsert = anesthesiaTableService.insertRows(getUser(), getContainer(), rowsToInsert, dbErrors, null, null); + // TODO: Check with labkey to verify there's no dbErrors to add to the anesthesiaRowsToDelete object below. Row deletion is not done in batch so this will work regardless. + List> anesthesiaRowsToDelete = anesthesiaTableService.deleteRows(getUser(), getContainer(), rowsToDelete, null, null); + tasksTableService.insertRows(getUser(), getContainer(), tasksToInsert, dbErrors, null, null); + tasksTableService.updateRows(getUser(), getContainer(), tasksToUpdate, tasksToUpdate, dbErrors, null, null); + + // 7c. Checks for errors in any row and aborts the insert. + if (dbErrors.hasErrors()) { + throw dbErrors; + } + + // 7d. Executes the Task to insert all rows via a batch transaction. + transaction.commit(); + response.put("success", true); + response.put("detailedResponse", "Database save successful."); + response.put("rowsInserted", anesthesiaRowsToInsert.size()); + response.put("rowsUpdated", anesthesiaRowsToDelete.size()); + return response; + + } + // Catches any errors. + catch (Exception e) { + _log.info("There was an issue modifying the anesthesiaRecovery dataset: " + e.getMessage()); + response.put("success", false); + response.put("detailedResponse", "Database save failed: " + e.getMessage()); + return response; + } + } + } + + public static class AnesthesiaBatchForm { + private List> rows; + + public List> getRows() { + return rows; + } + + public void setRows(List> rows) { + this.rows = rows; + } + } + + + + } diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java index 9c7f894d4..c68e93cf0 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/WNPRC_EHRModule.java @@ -394,6 +394,7 @@ public void registerNotifications() { new ClinpathResultAlertsRevamp(this), new LargeInfantAlertsRevamp(this), new OverdueWeightAlertsRevamp(this), + new AnesthesiaRecoveryReviewNotification(this), new SiteErrorAlertsRevamp(this), new WeightAlertsRevamp(this) ); diff --git a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java new file mode 100644 index 000000000..3e39d0475 --- /dev/null +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/AnesthesiaRecoveryReviewNotification.java @@ -0,0 +1,204 @@ +package org.labkey.wnprc_ehr.notification; + +import org.labkey.api.data.CompareType; +import org.labkey.api.data.Container; +import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.module.Module; +import org.labkey.api.security.User; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class AnesthesiaRecoveryReviewNotification extends AbstractEHRNotification { + // Class Variables + NotificationToolkit notificationToolkit = new NotificationToolkit(); + NotificationToolkit.DateToolkit dateToolkit = new NotificationToolkit.DateToolkit(); + NotificationToolkit.StyleToolkit styleToolkit = new NotificationToolkit.StyleToolkit(); + + + + + + // Constructors + + /** + * This constructor is used to register the notification in WNPRC_EHRModule.java. + * + * @param owner + */ + public AnesthesiaRecoveryReviewNotification(Module owner) { super(owner); } + + + + + + // Notification Details + @Override + public String getName() { return "Anesthesia Recovery Review"; } + + @Override + public String getDescription() { + return "This report is designed to identify any issues with the Anesthesia Recoveries dataset."; + } + @Override + public String getEmailSubject(Container c) { + return "Anesthesia Recovery Review: " + dateToolkit.getCurrentTime(); + } + @Override + public String getScheduleDescription() { return "Daily at 1:00PM and 3:00PM"; } + @Override + public String getCronString() { return notificationToolkit.createCronString("0", "13,15", "*"); } + @Override + public String getCategory() { return "iOS App Notifications"; } + + + + + + // Message Creation + public String getMessageBodyHTML(Container c, User u) { + // Creates variables & gets data. + final StringBuilder messageBody = new StringBuilder(); + AnesthesiaRecoveryReviewNotificationObject myRecoveriesObject = new AnesthesiaRecoveryReviewNotificationObject(c, u); + AnesthesiaRecoveryReviewReviewRequiredObject myRequiredReviewsObject = new AnesthesiaRecoveryReviewReviewRequiredObject(c, u); + + // Creates CSS. + messageBody.append(styleToolkit.beginStyle()); + messageBody.append(styleToolkit.setBasicTableStyle()); + messageBody.append(styleToolkit.setHeaderRowBackgroundColor("#d9d9d9")); + messageBody.append(styleToolkit.endStyle()); + + // Begins message info. + messageBody.append("

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

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


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


", myRequiredReviewsObject.reviewRequiredRecoveriesURL)); + } + } + + // 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 a558350d6..1f11aa55a 100644 --- a/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NotificationToolkit.java +++ b/WNPRC_EHR/src/org/labkey/wnprc_ehr/notification/NotificationToolkit.java @@ -870,6 +870,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. @@ -903,22 +904,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 91ea30c8c..64b112d1c 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); + } +}