diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c8845b293..ea91e096e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: [ push, pull_request, workflow_dispatch ] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.wvr/build-config.js b/.wvr/build-config.js index 2c3b7708d5..5d680227af 100644 --- a/.wvr/build-config.js +++ b/.wvr/build-config.js @@ -41,7 +41,8 @@ const config = { return content .toString() .replace('${AUTH_SERVICE_URL}', 'window.location.protocol + \'//\' + window.location.host + window.location.base + \'/mock/auth\'') - .replace('${STOMP_DEBUG}', 'false'); + .replace('${STOMP_DEBUG}', 'false') + .replace('${LOCAL_AUTHENTICATION}', 'true'); }, }, ], diff --git a/README.md b/README.md index a381bebeac..c7551f6ef9 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ $ mvn clean spring-boot:run -Dproduction $ mvn clean package -DskipTests -Dproduction -Dassets.uri=file:/opt/vireo/ -Dconfig.uri=file:/opt/vireo/config/ ``` -If build succeeds, you should have both a `vireo-4.2.11.war` and a `vireo-4.2.11-install.zip` in the `target/` directory. When building for production required static assets are copied into the packaged war file and the index.html template is optimized for production. For development a symlink is used to allow the application to access required static assets. +If build succeeds, you should have both a `vireo-4.3.0.war` and a `vireo-4.3.0-install.zip` in the `target/` directory. When building for production required static assets are copied into the packaged war file and the index.html template is optimized for production. For development a symlink is used to allow the application to access required static assets. #### Apache Reverse Proxy Config @@ -117,7 +117,7 @@ Unzip package into preferred directory (or any directory you choose): ```bash $ cd /opt/vireo -$ unzip vireo-4.2.11-install.zip +$ unzip vireo-4.3.0-install.zip ``` ### Directory Structure of installed package @@ -190,13 +190,13 @@ ln -s /opt/vireo/webapp /opt/tomcat/webapps/ROOT Copy war file into Tomcat webapps directory (your location may vary -- this is an example): ```bash -$ cp ~/vireo-4.2.11.war /usr/local/tomcat/webapps/vireo.war +$ cp ~/vireo-4.3.0.war /usr/local/tomcat/webapps/vireo.war ``` or as root: ```bash -$ cp ~/vireo-4.2.11.war /usr/local/tomcat/webapps/ROOT.war +$ cp ~/vireo-4.3.0.war /usr/local/tomcat/webapps/ROOT.war ``` **if not specifying assets.uri during build the assets will be stored under the vireo webapp's classpath, /opt/tomcat/webapps/vireo/WEB-INF/classes** @@ -209,7 +209,7 @@ $ cp ~/vireo-4.2.11.war /usr/local/tomcat/webapps/ROOT.war ## Running WAR as a stand-alone Spring Boot application ```bash -java -jar target/vireo-4.2.11.war +java -jar target/vireo-4.3.0.war ```
(back to top)
diff --git a/build/appConfig.js.template b/build/appConfig.js.template index 1e990074fb..e3786fe5cb 100644 --- a/build/appConfig.js.template +++ b/build/appConfig.js.template @@ -1,6 +1,6 @@ var appConfig = { - 'version': '4.2.11', + 'version': '4.3.0', 'allowAnonymous': true, 'anonymousRole': 'ROLE_ANONYMOUS', @@ -40,7 +40,7 @@ var appConfig = { When set to the boolean false, a modal is not used and Shibboleth login is automatically attempted and no registration link is shown. When set to the string 'alternate' to hide the local login (and registration) as if localAuthentication is false and provide an alterate (hidden) login page for local authentication. */ - 'localAuthentication': true, + 'localAuthentication': ${LOCAL_AUTHENTICATION}, /* Designate predicates that require date/times value filtering on save. diff --git a/example.env b/example.env index 9614af7d35..2b71d531ab 100644 --- a/example.env +++ b/example.env @@ -4,7 +4,7 @@ ############################## IMAGE_HOST=127.0.0.1 -IMAGE_VERSION=4.2.11 +IMAGE_VERSION=4.3.0 SERVICE_PROJECT=tdl SERVICE_PATH=vireo @@ -12,6 +12,8 @@ NODE_ENV=production STOMP_DEBUG=false +LOCAL_AUTHENTICATION=true + AUTH_STRATEGY=weaverAuth AUTH_SERVICE_URL="window.location.protocol + '//' + window.location.host + window.location.base + '/mock/auth'" diff --git a/package.json b/package.json index 9ebdacf493..414d152b2f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vireo", "private": false, - "version": "4.2.11", + "version": "4.3.0", "description": "Vireo 4", "homepage": "https://github.com/TexasDigitalLibrary/Vireo", "repository": { @@ -22,7 +22,7 @@ "build": "wvr build --clean" }, "dependencies": { - "@wvr/core": "2.3.0-rc3", + "@wvr/core": "2.3.0-rc5", "angular-ui-tinymce": "0.0.19", "file-saver": "2.0.5", "ng-csv": "0.3.6", diff --git a/pom.xml b/pom.xml index 5b17269277..4670972292 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.tdl vireo - 4.2.11 + 4.3.0 Vireo Vireo Thesis and Dissertation Submission System @@ -149,6 +149,18 @@ + + org.apache.poi + poi-ooxml + 5.2.3 + + + org.apache.logging.log4j + log4j-api + + + + org.apache.tika tika-core diff --git a/src/main/java/org/tdl/vireo/ApplicationConstants.java b/src/main/java/org/tdl/vireo/ApplicationConstants.java new file mode 100644 index 0000000000..64eacd8964 --- /dev/null +++ b/src/main/java/org/tdl/vireo/ApplicationConstants.java @@ -0,0 +1,11 @@ +package org.tdl.vireo; + +public class ApplicationConstants { + + public static final String UNKNOWN = "Unknown"; + + private ApplicationConstants() { + + } + +} diff --git a/src/main/java/org/tdl/vireo/config/constant/ConfigurationName.java b/src/main/java/org/tdl/vireo/config/constant/ConfigurationName.java index c8381620f5..ff93fa2b6f 100644 --- a/src/main/java/org/tdl/vireo/config/constant/ConfigurationName.java +++ b/src/main/java/org/tdl/vireo/config/constant/ConfigurationName.java @@ -98,9 +98,6 @@ public class ConfigurationName { /** Custom CSS */ public final static String LEFT_LOGO = "left_logo"; - /** Custom CSS */ - public final static String RIGTH_LOGO = "right_logo"; - /** Custom CSS */ public final static String CUSTOM_CSS = "custom_css"; diff --git a/src/main/java/org/tdl/vireo/controller/OrganizationController.java b/src/main/java/org/tdl/vireo/controller/OrganizationController.java index 0fc46f64d5..de58716ac5 100644 --- a/src/main/java/org/tdl/vireo/controller/OrganizationController.java +++ b/src/main/java/org/tdl/vireo/controller/OrganizationController.java @@ -13,6 +13,9 @@ import java.util.Map; import java.util.Optional; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -23,9 +26,11 @@ import org.tdl.vireo.exception.ComponentNotPresentOnOrgException; import org.tdl.vireo.exception.SystemEmailRuleNotDeleteableException; import org.tdl.vireo.exception.WorkflowStepNonOverrideableException; +import org.tdl.vireo.model.Action; import org.tdl.vireo.model.EmailRecipient; import org.tdl.vireo.model.EmailTemplate; -import org.tdl.vireo.model.EmailWorkflowRule; +import org.tdl.vireo.model.EmailWorkflowRuleByAction; +import org.tdl.vireo.model.EmailWorkflowRuleByStatus; import org.tdl.vireo.model.FieldPredicate; import org.tdl.vireo.model.Organization; import org.tdl.vireo.model.Submission; @@ -33,6 +38,7 @@ import org.tdl.vireo.model.WorkflowStep; import org.tdl.vireo.model.repo.AbstractEmailRecipientRepo; import org.tdl.vireo.model.repo.EmailTemplateRepo; +import org.tdl.vireo.model.repo.EmailWorkflowRuleByActionRepo; import org.tdl.vireo.model.repo.EmailWorkflowRuleRepo; import org.tdl.vireo.model.repo.FieldPredicateRepo; import org.tdl.vireo.model.repo.OrganizationRepo; @@ -42,10 +48,6 @@ import org.tdl.vireo.view.ShallowOrganizationView; import org.tdl.vireo.view.TreeOrganizationView; -import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - import edu.tamu.weaver.response.ApiResponse; import edu.tamu.weaver.response.ApiView; import edu.tamu.weaver.validation.aspect.annotation.WeaverValidatedModel; @@ -70,6 +72,9 @@ public class OrganizationController { @Autowired private EmailWorkflowRuleRepo emailWorkflowRuleRepo; + @Autowired + private EmailWorkflowRuleByActionRepo emailWorkflowRuleByActionRepo; + @Autowired private SubmissionRepo submissionRepo; @@ -254,7 +259,7 @@ public ApiResponse addEmailWorkflowRule(@PathVariable Long requestingOrgId, @Req if (emailRecipient == null) { response = new ApiResponse(ERROR, "Could not create recipient."); } else { - EmailWorkflowRule newEmailWorkflowRule = emailWorkflowRuleRepo.create(submissionStatus, emailRecipient, emailTemplate); + EmailWorkflowRuleByStatus newEmailWorkflowRule = emailWorkflowRuleRepo.create(submissionStatus, emailRecipient, emailTemplate); org.addEmailWorkflowRule(newEmailWorkflowRule); organizationRepo.update(org); @@ -275,7 +280,7 @@ public ApiResponse editEmailWorkflowRule(@PathVariable Long requestingOrgId, @Pa JsonNode recipientNode = objectMapper.convertValue(data, JsonNode.class).get("recipient"); EmailTemplate emailTemplate = emailTemplateRepo.findById(Long.valueOf((Integer) data.get("templateId"))).get(); - EmailWorkflowRule emailWorkflowRuleToUpdate = emailWorkflowRuleRepo.findById(emailWorkflowRuleId).get(); + EmailWorkflowRuleByStatus emailWorkflowRuleToUpdate = emailWorkflowRuleRepo.findById(emailWorkflowRuleId).get(); EmailRecipient emailRecipient = buildRecipient(recipientNode); @@ -301,7 +306,7 @@ public ApiResponse editEmailWorkflowRule(@PathVariable Long requestingOrgId, @Pa public ApiResponse removeEmailWorkflowRule(@PathVariable Long requestingOrgId, @PathVariable Long emailWorkflowRuleId) throws SystemEmailRuleNotDeleteableException { Organization org = organizationRepo.read(requestingOrgId); - EmailWorkflowRule rule = emailWorkflowRuleRepo.findById(emailWorkflowRuleId).get(); + EmailWorkflowRuleByStatus rule = emailWorkflowRuleRepo.findById(emailWorkflowRuleId).get(); if (rule.isSystem()) { throw new SystemEmailRuleNotDeleteableException(); @@ -320,7 +325,7 @@ public ApiResponse removeEmailWorkflowRule(@PathVariable Long requestingOrgId, @ @PreAuthorize("hasRole('MANAGER')") public ApiResponse changeEmailWorkflowRuleActivation(@PathVariable Long requestingOrgId, @PathVariable Long emailWorkflowRuleId) { - EmailWorkflowRule rule = emailWorkflowRuleRepo.findById(emailWorkflowRuleId).get(); + EmailWorkflowRuleByStatus rule = emailWorkflowRuleRepo.findById(emailWorkflowRuleId).get(); rule.isDisabled(!rule.isDisabled()); @@ -331,6 +336,99 @@ public ApiResponse changeEmailWorkflowRuleActivation(@PathVariable Long requesti return new ApiResponse(SUCCESS); } + @PreAuthorize("hasRole('MANAGER')") + @RequestMapping(value = "/{requestingOrgId}/add-email-workflow-rule-by-action", method = POST) + public ApiResponse addEmailWorkflowRuleByAction(@PathVariable Long requestingOrgId, @RequestBody Map data) { + + ApiResponse response = new ApiResponse(SUCCESS); + + Organization org = organizationRepo.read(requestingOrgId); + Action action = Action.valueOf((String) data.get("action")); + JsonNode recipientNode = objectMapper.convertValue(data, JsonNode.class).get("recipient"); + EmailTemplate emailTemplate = emailTemplateRepo.findById(Long.valueOf((Integer) data.get("templateId"))).get(); + + EmailRecipient emailRecipient = buildRecipient(recipientNode); + + if (emailRecipient == null) { + response = new ApiResponse(ERROR, "Could not create recipient."); + } else { + EmailWorkflowRuleByAction newEmailWorkflowRule = emailWorkflowRuleByActionRepo.create(action, emailRecipient, emailTemplate); + org.addEmailWorkflowRuleByAction(newEmailWorkflowRule); + organizationRepo.update(org); + + HashMap payload = new HashMap(); + payload.put("id", newEmailWorkflowRule.getId()); + response.setPayload(payload); + } + + return response; + } + + @RequestMapping("/{requestingOrgId}/edit-email-workflow-rule-by-action/{emailWorkflowRuleId}") + @PreAuthorize("hasRole('MANAGER')") + public ApiResponse editEmailWorkflowRuleByAction(@PathVariable Long requestingOrgId, @PathVariable Long emailWorkflowRuleId, @RequestBody Map data) { + + ApiResponse response = new ApiResponse(SUCCESS); + + JsonNode recipientNode = objectMapper.convertValue(data, JsonNode.class).get("recipient"); + EmailTemplate emailTemplate = emailTemplateRepo.findById(Long.valueOf((Integer) data.get("templateId"))).get(); + + EmailWorkflowRuleByAction emailWorkflowRuleToUpdate = emailWorkflowRuleByActionRepo.findById(emailWorkflowRuleId).get(); + + EmailRecipient emailRecipient = buildRecipient(recipientNode); + + if (emailRecipient == null) { + response = new ApiResponse(ERROR, "Could not create recipient."); + } else { + + emailWorkflowRuleToUpdate.setEmailTemplate(emailTemplate); + emailWorkflowRuleToUpdate.setEmailRecipient(emailRecipient); + + // TODO emailWorkflowRuleRepo.update(emailWorkflowRuleToUpdate); + emailWorkflowRuleByActionRepo.save(emailWorkflowRuleToUpdate); + + // TODO: Is this needed? + organizationRepo.broadcast(requestingOrgId); + } + + return response; + } + + @RequestMapping("/{requestingOrgId}/remove-email-workflow-rule-by-action/{emailWorkflowRuleId}") + @PreAuthorize("hasRole('MANAGER')") + public ApiResponse removeEmailWorkflowRuleByAction(@PathVariable Long requestingOrgId, @PathVariable Long emailWorkflowRuleId) throws SystemEmailRuleNotDeleteableException { + + Organization org = organizationRepo.read(requestingOrgId); + EmailWorkflowRuleByAction rule = emailWorkflowRuleByActionRepo.findById(emailWorkflowRuleId).get(); + + if (rule.isSystem()) { + throw new SystemEmailRuleNotDeleteableException(); + } else { + + org.removeEmailWorkflowRuleByAction(rule); + emailWorkflowRuleByActionRepo.delete(rule); + + organizationRepo.update(org); + + return new ApiResponse(SUCCESS); + } + } + + @RequestMapping("/{requestingOrgId}/change-email-workflow-rule-by-action-activation/{emailWorkflowRuleId}") + @PreAuthorize("hasRole('MANAGER')") + public ApiResponse changeEmailWorkflowRuleByActionActivation(@PathVariable Long requestingOrgId, @PathVariable Long emailWorkflowRuleId) { + + EmailWorkflowRuleByAction rule = emailWorkflowRuleByActionRepo.findById(emailWorkflowRuleId).get(); + + rule.isDisabled(!rule.isDisabled()); + + emailWorkflowRuleByActionRepo.save(rule); + + organizationRepo.broadcast(requestingOrgId); + + return new ApiResponse(SUCCESS); + } + @RequestMapping("/{requestingOrgId}/count-submissions") @PreAuthorize("hasRole('MANAGER')") public ApiResponse countSubmissions(@PathVariable Long requestingOrgId) { diff --git a/src/main/java/org/tdl/vireo/controller/SubmissionController.java b/src/main/java/org/tdl/vireo/controller/SubmissionController.java index 806b222009..103759de1d 100644 --- a/src/main/java/org/tdl/vireo/controller/SubmissionController.java +++ b/src/main/java/org/tdl/vireo/controller/SubmissionController.java @@ -4,8 +4,12 @@ import static edu.tamu.weaver.response.ApiStatus.INVALID; import static edu.tamu.weaver.response.ApiStatus.SUCCESS; +import javax.servlet.http.HttpServletResponse; + import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -15,21 +19,29 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import javax.servlet.http.HttpServletResponse; - +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; -import org.apache.poi.hssf.usermodel.HSSFRow; -import org.apache.poi.hssf.usermodel.HSSFSheet; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.CountingOutputStream; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -39,7 +51,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.web.PageableDefault; -import org.springframework.http.HttpStatus; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; @@ -52,8 +63,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.tdl.vireo.ApplicationConstants; +import org.tdl.vireo.exception.BatchExportException; import org.tdl.vireo.exception.DepositException; import org.tdl.vireo.exception.OrganizationDoesNotAcceptSubmissionsException; +import org.tdl.vireo.model.Action; +import org.tdl.vireo.model.ActionLog; import org.tdl.vireo.model.CustomActionValue; import org.tdl.vireo.model.DepositLocation; import org.tdl.vireo.model.Embargo; @@ -93,11 +108,6 @@ import org.tdl.vireo.utility.TemplateUtility; import org.tdl.vireo.view.FieldValueSubmissionView; -import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - import edu.tamu.weaver.auth.annotation.WeaverCredentials; import edu.tamu.weaver.auth.annotation.WeaverUser; import edu.tamu.weaver.auth.model.Credentials; @@ -246,7 +256,8 @@ public ApiResponse createSubmission(@WeaverUser User user, @WeaverCredentials Cr credentials, customActionDefinitionRepo.findAll() ); - actionLogRepo.createPublicLog(submission, user, "Submission created."); + submissionEmailService.sendWorkflowEmails(user, submission.getId()); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Submission created."); return new ApiResponse(SUCCESS, submission.getId()); } @@ -285,12 +296,12 @@ public ApiResponse addComment(@WeaverUser User user, @PathVariable Long submissi } else{ String subject = (String) data.get("subject"); String templatedMessage = templateUtility.compileString((String) data.get("message"), submission); - actionLogRepo.createPublicLog(submission, user, subject + ": " + templatedMessage); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, subject + ": " + templatedMessage); } } else { String subject = (String) data.get("subject"); String templatedMessage = templateUtility.compileString((String) data.get("message"), submission); - actionLogRepo.createPrivateLog(submission, user, subject + ": " + templatedMessage); + actionLogRepo.createPrivateLog(Action.UNDETERMINED, submission, user, subject + ": " + templatedMessage); } return new ApiResponse(SUCCESS); @@ -325,12 +336,12 @@ public ApiResponse batchComment(@WeaverUser User user, @RequestBody Map packager = packagerUtility.getPackager(packagerName); + private void processBatchExport( + HttpServletResponse response, + User user, + String packagerName, + NamedSearchFilterGroup filter + ) throws BatchExportException { - List columns = filter.getColumnsFlag() ? filter.getSavedColumns() : user.getSubmissionViewColumns(); - switch (packagerName.trim()) { - case "Excel": - List submissions = submissionRepo.batchDynamicSubmissionQuery(filter, columns); + final AbstractPackager packager = packagerUtility.getPackager(packagerName); - HSSFWorkbook workbook = new HSSFWorkbook(); + final boolean useFilter = filter != null && + filter.getSavedColumns() != null && + filter.getColumnsFlag() != null && + filter.getColumnsFlag().toString().equals(Boolean.TRUE.toString()); - HSSFSheet worksheet = workbook.createSheet(); + final List columns = useFilter + ? filter.getSavedColumns() + : user.getSubmissionViewColumns(); - int rowCount = 0; + long contentLength = 0; - HSSFRow header = worksheet.createRow(rowCount++); + Path export = null; - for (int i = 0; i < columns.size(); i++) { - SubmissionListColumn column = columns.get(i); - header.createCell(i).setCellValue(column.getTitle()); - } + LOG.info("Beginning Export with packager {}", packagerName.trim()); - for (Submission submission : submissions) { - ExportPackage exportPackage = packagerUtility.packageExport(packager, submission, columns); - if (exportPackage.isMap()) { - Map rowData = (Map) exportPackage.getPayload(); - HSSFRow row = worksheet.createRow(rowCount++); - for (int i = 0; i < columns.size(); i++) { - SubmissionListColumn column = columns.get(i); - row.createCell(i).setCellValue(rowData.get(column.getTitle())); - } - } - } + try { + export = Files.createTempFile(packagerName, packager.getFileExtension()); - for (int i = 0; i < columns.size(); i++) { - worksheet.autoSizeColumn(i); - } + try ( + FileOutputStream tempFileOS = new FileOutputStream(export.toFile()); + CountingOutputStream tempFileCountingOS = new CountingOutputStream(tempFileOS); + ) { - response.setContentType(packager.getMimeType()); - response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); - workbook.write(response.getOutputStream()); + switch (packagerName.trim()) { + case "Excel": - break; - case "MarcXML21": - case "Marc21": - ByteArrayOutputStream sos = new ByteArrayOutputStream(); + // Create a streaming workbook with a window size of 100 rows + // (only this many rows will be kept in memory at once) + try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) { + List submissions = submissionRepo.batchDynamicSubmissionQuery(filter, columns); - try { - ZipOutputStream zos = new ZipOutputStream(sos, StandardCharsets.UTF_8); - - for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { - - StringBuilder contentsText = new StringBuilder(); - ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); - if (exportPackage.isMap()) { - for (Map.Entry fileEntry : ((Map) exportPackage.getPayload()).entrySet()) { - if (packagerName.equals("MarcXML21")) { - zos.putNextEntry(new ZipEntry("MarcXML21/" + fileEntry.getKey())); - } else { - zos.putNextEntry(new ZipEntry(fileEntry.getKey())); + // Enable compression for temporary files + workbook.setCompressTempFiles(true); + + Sheet worksheet = workbook.createSheet(); + + int rowCount = 0; + + // Create header row + Row header = worksheet.createRow(rowCount++); + for (int i = 0; i < columns.size(); i++) { + SubmissionListColumn column = columns.get(i); + header.createCell(i).setCellValue(column.getTitle()); + } + + // Stream data rows + for (Submission submission : submissions) { + ExportPackage exportPackage = packagerUtility.packageExport(packager, submission, columns); + if (exportPackage.isMap()) { + Map rowData = (Map) exportPackage.getPayload(); + Row row = worksheet.createRow(rowCount++); + for (int i = 0; i < columns.size(); i++) { + SubmissionListColumn column = columns.get(i); + Cell cell = row.createCell(i); + cell.setCellValue(rowData.get(column.getTitle())); + } } - contentsText.append("MD " + fileEntry.getKey() + "\n"); - zos.write(Files.readAllBytes(fileEntry.getValue().toPath())); - zos.closeEntry(); } - } - } - zos.close(); - response.getOutputStream().write(sos.toByteArray()); - response.setContentType(packager.getMimeType()); - response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); - } catch (Exception e) { - handleBatchExportError(e, response); - } - break; - case "ProQuest": - ByteArrayOutputStream sos_pq = new ByteArrayOutputStream(); + // Auto-sizing is expensive with SXSSF as it can't look back at rows + // that have been flushed to disk. If needed, track column widths manually. + // If you must use auto-sizing, you'll need to track max width as you go - try { - ZipOutputStream zos = new ZipOutputStream(sos_pq, StandardCharsets.UTF_8); - for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { - List fieldValues = submission.getFieldValuesByPredicateValue("first_name"); - Optional firstNameOpt = fieldValues.size() > 0 ? Optional.of(fieldValues.get(0).getValue()) : Optional.empty(); - String firstName = firstNameOpt.isPresent() ? firstNameOpt.get() : ""; - firstName = firstName.substring(0,1).toUpperCase()+firstName.substring(1); - fieldValues = submission.getFieldValuesByPredicateValue("last_name"); - Optional lastNameOpt = fieldValues.size() > 0 ? Optional.of(fieldValues.get(0).getValue()) : Optional.empty(); - String lastName = lastNameOpt.isPresent() ? lastNameOpt.get() : ""; - lastName = lastName.substring(0,1).toUpperCase()+lastName.substring(1); - String personName = lastName+"_"+firstName; - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (ZipOutputStream b = new ZipOutputStream(baos)){ - ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); - if (exportPackage.isMap()) { - for (Map.Entry fileEntry : ((Map) exportPackage.getPayload()).entrySet()) { - b.putNextEntry(new ZipEntry(personName+"_DATA.xml")); - b.write(Files.readAllBytes(fileEntry.getValue().toPath())); - b.closeEntry(); + // Write to temp file via counting output stream + workbook.write(tempFileCountingOS); + + // Dispose of temporary files + workbook.dispose(); + } + + // Set response headers + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "inline; filename=" + packagerName + ".xlsx"); + + break; + case "MarcXML21": + case "Marc21": + + try (ZipOutputStream zos = new ZipOutputStream(tempFileCountingOS, StandardCharsets.UTF_8)) { + for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { + + StringBuilder contentsText = new StringBuilder(); + ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); + if (exportPackage.isMap()) { + for (Map.Entry fileEntry : ((Map) exportPackage.getPayload()).entrySet()) { + if (packagerName.equals("MarcXML21")) { + zos.putNextEntry(new ZipEntry("MarcXML21/" + fileEntry.getKey())); + } else { + zos.putNextEntry(new ZipEntry(fileEntry.getKey())); + } + contentsText.append("MD " + fileEntry.getKey() + "\n"); + byte[] fileBytes = Files.readAllBytes(fileEntry.getValue().toPath()); + zos.write(fileBytes); + zos.closeEntry(); + } } } - // LICENSES - for (FieldValue ldfv : submission.getLicenseDocumentFieldValues()) { - Path path = assetService.getAssetsAbsolutePath(ldfv.getValue()); - byte[] fileBytes = Files.readAllBytes(path); - int sfxIndx; - String licFileName = ldfv.getFileName(); - if((sfxIndx = licFileName.indexOf("."))>0){ - licFileName = licFileName.substring(0,sfxIndx).toUpperCase()+licFileName.substring(sfxIndx); + } + + // Set response headers + response.setContentType(packager.getMimeType()); + response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); + + break; + case "ProQuest": + + try (ZipOutputStream zos = new ZipOutputStream(tempFileCountingOS, StandardCharsets.UTF_8)) { + for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { + String firstName = getName(submission, "first_name"); + + String lastName = getName(submission, "last_name"); + + String personEntry = lastName + "_" + firstName + "_" + submission.getId(); + + try ( + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream izos = new ZipOutputStream(baos); + ) { + ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); + if (exportPackage.isMap()) { + for (Map.Entry fileEntry : ((Map) exportPackage.getPayload()).entrySet()) { + izos.putNextEntry(new ZipEntry(personEntry + "_DATA.xml")); + Path path = fileEntry.getValue().toPath(); + byte[] fileBytes = Files.readAllBytes(path); + izos.write(fileBytes); + izos.closeEntry(); + } + } + // LICENSES + Set licenseFileNames = new HashSet<>(); + for (FieldValue ldfv : submission.getLicenseDocumentFieldValues()) { + Path path = assetService.getAssetsAbsolutePath(ldfv.getValue()); + byte[] fileBytes = Files.readAllBytes(path); + + int sfxIndx = ldfv.getFileName().indexOf("."); + + String fileName = sfxIndx > 0 + ? ldfv.getFileName().substring(0, sfxIndx) + : ldfv.getFileName(); + + String fileExt = sfxIndx > 0 + ? ldfv.getFileName().substring(sfxIndx) + : StringUtils.EMPTY; + + String fullFileName = fileName.toUpperCase() + fileExt; + + int i = 0; + while (licenseFileNames.contains(fullFileName)) { + fullFileName = String.format("%s_%s%s", fileName.toUpperCase(), ++i, fileExt); + } + licenseFileNames.add(fullFileName); + izos.putNextEntry(new ZipEntry(personEntry + "_permission/" + fullFileName)); + izos.write(fileBytes); + izos.closeEntry(); + } + // PRIMARY_DOC + FieldValue primaryDoc = submission.getPrimaryDocumentFieldValue(); + if (primaryDoc != null && StringUtils.isNotEmpty(primaryDoc.getValue())) { + Path path = assetService.getAssetsAbsolutePath(primaryDoc.getValue()); + byte[] fileBytes = Files.readAllBytes(path); + String fName = primaryDoc.getFileName(); + int fNameIndx = fName.indexOf("."); + String fType = ""; // default + if (fNameIndx > 0) { + fType = fName.substring(fNameIndx); + } + izos.putNextEntry(new ZipEntry(personEntry + fType)); + izos.write(fileBytes); + izos.closeEntry(); + } + + izos.finish(); // Finish writing to the inner zip + izos.flush(); // Flush any buffered data + + zos.putNextEntry(new ZipEntry("upload_" + personEntry + ".zip")); + zos.write(baos.toByteArray()); + zos.closeEntry(); } - b.putNextEntry(new ZipEntry(personName+"_permission/"+licFileName)); - b.write(fileBytes); - b.closeEntry(); } - // PRIMARY_DOC - FieldValue primaryDoc = submission.getPrimaryDocumentFieldValue(); - Path path = assetService.getAssetsAbsolutePath(primaryDoc.getValue()); - byte[] fileBytes = Files.readAllBytes(path); - String fName = primaryDoc.getFileName(); - int fNameIndx = fName.indexOf("."); - String fType = "";//default - if(fNameIndx>0){ - fType = fName.substring(fNameIndx); + } + + // Set response headers + response.setContentType(packager.getMimeType()); + response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); + + break; + case "DSpaceMETS": + + try (ZipOutputStream zos = new ZipOutputStream(tempFileCountingOS)) { + for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { + ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); + File exportFile = (File) exportPackage.getPayload(); + byte[] fileBytes = FileUtils.readFileToByteArray(exportFile); + zos.putNextEntry(new ZipEntry(exportFile.getName())); + zos.write(fileBytes); + zos.closeEntry(); } - b.putNextEntry(new ZipEntry(personName+fType)); - b.write(fileBytes); - b.closeEntry(); } - zos.putNextEntry(new ZipEntry("upload_"+personName+".zip")); - baos.close(); - zos.write(baos.toByteArray()); - zos.closeEntry(); - } - zos.close(); - response.setContentType(packager.getMimeType()); - response.getOutputStream().write(sos_pq.toByteArray()); - response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); - } catch (Exception e) { - handleBatchExportError(e, response); - } - break; - case "DSpaceMETS": - ByteArrayOutputStream sos_mets = new ByteArrayOutputStream(); - try { - ZipOutputStream zos = new ZipOutputStream(sos_mets); - - for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { - ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); - File exportFile = (File) exportPackage.getPayload(); - byte[] fileBytes = FileUtils.readFileToByteArray(exportFile); - zos.putNextEntry(new ZipEntry(exportFile.getName())); - zos.write(fileBytes); - zos.closeEntry(); - } - zos.close(); - response.getOutputStream().write(sos_mets.toByteArray()); - response.setContentType(packager.getMimeType()); - response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); - } catch (Exception e) { - handleBatchExportError(e, response); - } - break; - case "DSpaceSimple": - ByteArrayOutputStream sosDss = new ByteArrayOutputStream(); - try { - ZipOutputStream zos = new ZipOutputStream(sosDss); - for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { - String submissionName = "submission_" + submission.getId() + "/"; - zos.putNextEntry(new ZipEntry(submissionName)); + // Set response headers + response.setContentType(packager.getMimeType()); + response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); + + break; + case "DSpaceSimple": + + try (ZipOutputStream zos = new ZipOutputStream(tempFileCountingOS)) { + for (Submission submission : submissionRepo.batchDynamicSubmissionQuery(filter, columns)) { + String submissionName = "submission_" + submission.getId() + "/"; + + zos.putNextEntry(new ZipEntry(submissionName)); + + StringBuilder contentsText = new StringBuilder(); + + ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); + + // METADATA + if (exportPackage.isMap()) { + for (Map.Entry fileEntry : ((Map) exportPackage.getPayload()).entrySet()) { + zos.putNextEntry(new ZipEntry(submissionName + fileEntry.getKey())); + contentsText.append(fileEntry.getKey() + "\n"); + byte[] fileBytes = Files.readAllBytes(fileEntry.getValue().toPath()); + zos.write(fileBytes); + zos.closeEntry(); + } + } + + // LICENSES + for (FieldValue ldfv : submission.getLicenseDocumentFieldValues()) { + Path path = assetService.getAssetsAbsolutePath(ldfv.getValue()); + byte[] fileBytes = Files.readAllBytes(path); + zos.putNextEntry(new ZipEntry(submissionName + ldfv.getFileName())); + contentsText.append(ldfv.getFileName() + "\tBUNDLE:LICENSE\n"); + zos.write(fileBytes); + zos.closeEntry(); + } + + // PRIMARY_DOC + FieldValue primaryDoc = submission.getPrimaryDocumentFieldValue(); + Path path = assetService.getAssetsAbsolutePath(primaryDoc.getValue()); + byte[] fileBytes = Files.readAllBytes(path); + zos.putNextEntry(new ZipEntry(submissionName + primaryDoc.getFileName())); + contentsText.append(primaryDoc.getFileName() + "\tBUNDLE:CONTENT\tprimary:true\n"); + zos.write(fileBytes); + zos.closeEntry(); - StringBuilder contentsText = new StringBuilder(); + // SUPPLEMENTAL_DOCS + List supplDocs = submission.getSupplementalAndSourceDocumentFieldValues(); + for (FieldValue supplDoc : supplDocs) { + Path supplPath = assetService.getAssetsAbsolutePath(supplDoc.getValue()); + byte[] supplFileBytes = Files.readAllBytes(supplPath); + zos.putNextEntry(new ZipEntry(submissionName+supplDoc.getFileName())); + contentsText.append(supplDoc.getFileName() + "\tBUNDLE:CONTENT\n"); + zos.write(supplFileBytes); + zos.closeEntry(); + } - ExportPackage exportPackage = packagerUtility.packageExport(packager, submission); + // CONTENTS_FILE + zos.putNextEntry(new ZipEntry(submissionName + "contents")); + zos.write(contentsText.toString().getBytes()); + zos.closeEntry(); - //METADATA - if (exportPackage.isMap()) { - for (Map.Entry fileEntry : ((Map) exportPackage.getPayload()).entrySet()) { - zos.putNextEntry(new ZipEntry(submissionName + fileEntry.getKey())); - contentsText.append(fileEntry.getKey()+"\n"); - zos.write(Files.readAllBytes(fileEntry.getValue().toPath())); + // close submission zip entry zos.closeEntry(); } } - // LICENSES - for (FieldValue ldfv : submission.getLicenseDocumentFieldValues()) { - Path path = assetService.getAssetsAbsolutePath(ldfv.getValue()); - byte[] fileBytes = Files.readAllBytes(path); - zos.putNextEntry(new ZipEntry(submissionName + ldfv.getFileName())); - contentsText.append(ldfv.getFileName()+"\tBUNDLE:LICENSE\n"); - zos.write(fileBytes); - zos.closeEntry(); - } + // Set response headers + response.setContentType(packager.getMimeType()); + response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); - // PRIMARY_DOC - FieldValue primaryDoc = submission.getPrimaryDocumentFieldValue(); - Path path = assetService.getAssetsAbsolutePath(primaryDoc.getValue()); - byte[] fileBytes = Files.readAllBytes(path); - zos.putNextEntry(new ZipEntry(submissionName+primaryDoc.getFileName())); - contentsText.append(primaryDoc.getFileName()+"\tBUNDLE:CONTENT\tprimary:true\n"); - zos.write(fileBytes); - zos.closeEntry(); - - // SUPPLEMENTAL_DOCS - List supplDocs = submission.getSupplementalAndSourceDocumentFieldValues(); - for (FieldValue supplDoc : supplDocs) { - Path supplPath = assetService.getAssetsAbsolutePath(supplDoc.getValue()); - byte[] supplFileBytes = Files.readAllBytes(supplPath); - zos.putNextEntry(new ZipEntry(submissionName+supplDoc.getFileName())); - contentsText.append(supplDoc.getFileName()+"\tBUNDLE:CONTENT\n"); - zos.write(supplFileBytes); - zos.closeEntry(); - } + break; + default: + throw new BatchExportException("No packager " + packagerName + " found!"); + } - // CONTENTS_FILE - zos.putNextEntry(new ZipEntry(submissionName + "contents")); - zos.write(contentsText.toString().getBytes()); - zos.closeEntry(); + // get byte count written to temp file + contentLength = tempFileCountingOS.getByteCount(); - zos.closeEntry(); + } + LOG.info("Packaging complete for {}", packagerName); + + // if no exception occurs and bytes written to temp file + if (contentLength > 0) { + LOG.info("Streaming response for {}", packagerName); + try (FileInputStream tempFileIS = new FileInputStream(export.toFile())) { + response.setHeader("Content-Length", Long.toString(contentLength)); + IOUtils.copyLarge(tempFileIS, response.getOutputStream()); } - zos.close(); - response.getOutputStream().write(sosDss.toByteArray()); - response.setContentType(packager.getMimeType()); - response.setHeader("Content-Disposition", "inline; filename=" + packagerName + "." + packager.getFileExtension()); - } catch (Exception e) { - handleBatchExportError(e, response); + LOG.info("Done streaming response for {}", packagerName); + } else { + throw new RuntimeException("No content to write to response output stream."); } - break; - default: - handleBatchExportError(new Exception("No packager " + packagerName + " found!"), response); + } catch (IOException e) { + elevateBatchExportException(e); + } catch (Exception e) { + // catch and elevate runtime exceptions to avoid going to the incorrect global exception handler + elevateBatchExportException(e); + } finally { + try { + if (export != null) { + LOG.info("Cleaning up temp files for {}", packagerName); + Files.deleteIfExists(export); + } + } catch (Exception e) { + elevateBatchExportException(e); + } } - - response.getOutputStream().close(); } @RequestMapping(value = "/batch-assign-to", method = RequestMethod.POST) @@ -785,7 +867,7 @@ private void processBatchExport(HttpServletResponse response, User user, String public ApiResponse batchAssignTo(@WeaverUser User user, @RequestBody User assignee) { submissionRepo.batchDynamicSubmissionQuery(user.getActiveFilter(), user.getSubmissionViewColumns()).forEach(sub -> { sub.setAssignee(assignee); - actionLogRepo.createPublicLog(sub, user, "Submission was assigned to " + assignee.getFirstName() + " " + assignee.getLastName() + "(" + assignee.getEmail() + ")"); + actionLogRepo.createPublicLog(Action.UNDETERMINED, sub, user, "Submission was assigned to " + assignee.getFirstName() + " " + assignee.getLastName() + "(" + assignee.getEmail() + ")"); submissionRepo.update(sub); }); return new ApiResponse(SUCCESS); @@ -808,6 +890,7 @@ public ApiResponse batchPublish(@WeaverUser User user, @PathVariable Long deposi String depositURL = depositor.deposit(depositLocation, exportPackage); submission.setDepositURL(depositURL); submission = submissionRepo.updateStatus(submission, submissionStatus, user); + submissionEmailService.sendWorkflowEmails(user, submission.getId()); } catch (Exception e) { throw new DepositException("Failed package export on submission " + submission.getId()); } @@ -841,7 +924,7 @@ public ApiResponse submitDate(@WeaverUser User user, @PathVariable("submissionId submission = submissionRepo.update(submission); SimpleDateFormat logdf = new SimpleDateFormat("MM/dd/yyyy"); - actionLogRepo.createPublicLog(submission, user, "Submission date set to: " + logdf.format(cal.getTime())); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Submission date set to: " + logdf.format(cal.getTime())); } else { response = new ApiResponse(ERROR, "Could not find a submission with ID " + submissionId); @@ -866,9 +949,9 @@ public ApiResponse assign(@WeaverUser User user, @PathVariable("submissionId") L submission = submissionRepo.update(submission); if (assignee == null) { - actionLogRepo.createPublicLog(submission, user, "Submission was unassigned"); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Submission was unassigned"); } else { - actionLogRepo.createPublicLog(submission, user, "Submission was assigned to " + assignee.getFirstName() + " " + assignee.getLastName() + "(" + assignee.getEmail() + ")"); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Submission was assigned to " + assignee.getFirstName() + " " + assignee.getLastName() + "(" + assignee.getEmail() + ")"); } } else { response = new ApiResponse(ERROR, "Could not find a submission with ID " + submissionId); @@ -894,7 +977,7 @@ public ApiResponse updateReviewerNotes(@WeaverUser User user, @PathVariable("sub String reviewerNotes = requestData.get("reviewerNotes"); submission.setReviewerNotes(reviewerNotes); submission = submissionRepo.update(submission); - actionLogRepo.createPrivateLog(submission, user, "Submission notes changed to \"" + reviewerNotes + "\""); + actionLogRepo.createPrivateLog(Action.UNDETERMINED, submission, user, "Submission notes changed to \"" + reviewerNotes + "\""); return new ApiResponse(SUCCESS, submission); } @@ -906,7 +989,9 @@ public ApiResponse setSubmissionNeedsCorrection(@WeaverUser User user, @PathVari String oldSubmissionStatusName = submission.getSubmissionStatus().getName(); submission.setSubmissionStatus(needsCorrectionStatus); submission = submissionRepo.update(submission); - actionLogRepo.createPublicLog(submission, user, "Submission status was changed from " + oldSubmissionStatusName + " to " + NEEDS_CORRECTION_SUBMISSION_STATUS_NAME); + submissionEmailService.sendWorkflowEmails(user, submission.getId()); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Submission status was changed from " + oldSubmissionStatusName + " to " + NEEDS_CORRECTION_SUBMISSION_STATUS_NAME); + return new ApiResponse(SUCCESS, submission); } @@ -921,7 +1006,7 @@ public ApiResponse setSubmissionCorrectionsReceived(@WeaverUser User user, @Path SubmissionStatus correctionsReceivedStatus = submissionStatusRepo.findByName(CORRECTIONS_RECEIVED_SUBMISSION_STATUS_NAME); submission.setSubmissionStatus(correctionsReceivedStatus); submission = submissionRepo.update(submission); - actionLogRepo.createPublicLog(submission, user, "Submission status was changed from " + oldSubmissionStatusName + " to " + CORRECTIONS_RECEIVED_SUBMISSION_STATUS_NAME); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Submission status was changed from " + oldSubmissionStatusName + " to " + CORRECTIONS_RECEIVED_SUBMISSION_STATUS_NAME); submissionEmailService.sendWorkflowEmails(user, submission.getId()); return new ApiResponse(SUCCESS, submission); } @@ -930,7 +1015,9 @@ public ApiResponse setSubmissionCorrectionsReceived(@WeaverUser User user, @Path @PreAuthorize("hasRole('STUDENT')") public ApiResponse addMessage(@WeaverUser User user, @PathVariable Long submissionId, @RequestBody String message) { Submission submission = submissionRepo.read(submissionId); - return new ApiResponse(SUCCESS, actionLogRepo.createPublicLog(submission, user, message)); + ActionLog actionLog = actionLogRepo.createPublicLog(Action.STUDENT_MESSAGE, submission, user, message); + submissionEmailService.sendActionEmails(submission, actionLog); + return new ApiResponse(SUCCESS, actionLog); } @Transactional(readOnly = true) @@ -983,7 +1070,7 @@ public ApiResponse uploadFile(@WeaverUser User user, @PathVariable Long submissi String uri = documentFolder + File.separator + hash + File.separator + System.currentTimeMillis() + "-" + fileName; assetService.write(file.getBytes(), uri); JsonNode fileInfo = assetService.getAssetFileInfo(uri, submission); - actionLogRepo.createPublicLog(submission, user, documentType + " file " + fileInfo.get("name").asText() + " (" + fileInfo.get("readableSize").asText() + ") uploaded"); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, documentType + " file " + fileInfo.get("name").asText() + " (" + fileInfo.get("readableSize").asText() + ") uploaded"); return new ApiResponse(SUCCESS, uri); } @@ -997,7 +1084,7 @@ public ApiResponse renameFile(@WeaverUser User user, @PathVariable Long submissi String newUri = oldUri.replace(oldUri.substring(oldUri.lastIndexOf(File.separator) + 1, oldUri.length()), System.currentTimeMillis() + "-" + newName); assetService.rename(oldUri, newUri); JsonNode fileInfo = assetService.getAssetFileInfo(newUri, submission); - actionLogRepo.createPublicLog(submission, user, documentType + " file " + fileInfo.get("name").asText() + " (" + fileInfo.get("readableSize").asText() + ") renamed"); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, documentType + " file " + fileInfo.get("name").asText() + " (" + fileInfo.get("readableSize").asText() + ") renamed"); return new ApiResponse(SUCCESS, newUri); } @@ -1026,7 +1113,7 @@ public ApiResponse removeFile(@WeaverUser User user, @PathVariable Long submissi } assetService.delete(uri); - actionLogRepo.createPublicLog(submission, user, documentType.substring(9).toUpperCase() + " file " + fileName + " (" + fileSize + ") removed"); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, documentType.substring(9).toUpperCase() + " file " + fileName + " (" + fileSize + ") removed"); } else { apiResponse = new ApiResponse(ERROR, "This is not your file to delete!"); } @@ -1047,7 +1134,7 @@ public ApiResponse archiveFile(@WeaverUser User user, @PathVariable Long submiss String newUri = oldUri.replace(oldUri.substring(oldUri.lastIndexOf(File.separator) + 1, oldUri.length()), System.currentTimeMillis() + "-archived-" + name); assetService.rename(oldUri, newUri); JsonNode fileInfo = assetService.getAssetFileInfo(newUri, submission); - actionLogRepo.createPublicLog(submission, user, "ARCHIVE - " + documentType + " file " + fileInfo.get("name").asText() + " (" + fileInfo.get("readableSize").asText() + ") archived"); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "ARCHIVE - " + documentType + " file " + fileInfo.get("name").asText() + " (" + fileInfo.get("readableSize").asText() + ") archived"); return new ApiResponse(SUCCESS, newUri); } @@ -1099,7 +1186,8 @@ public ApiResponse updateAdvisorApproval(@PathVariable Long submissionId, @Reque } if (message != null) { - actionLogRepo.createAdvisorPublicLog(submission, "Advisor comments : " + message); + ActionLog actionLog = actionLogRepo.createAdvisorPublicLog(Action.ADVISOR_MESSAGE, submission, "Advisor comments : " + message); + submissionEmailService.sendActionEmails(submission, actionLog); } return new ApiResponse(SUCCESS, submission); @@ -1108,22 +1196,43 @@ public ApiResponse updateAdvisorApproval(@PathVariable Long submissionId, @Reque private void processAdvisorAction(String type, Boolean approveStatus, Submission submission) { String approveAdvisorMessage; + Action action; if (approveStatus == true) { approveAdvisorMessage = "The committee approved the " + type + "."; + action = type.equals("Application") ? Action.ADVISOR_APPROVE_SUBMISSION : Action.ADVISOR_APPROVE_EMBARGO; } else { approveAdvisorMessage = "The committee rejected the " + type + "."; + action = type.equals("Application") ? Action.ADVISOR_CLEAR_APPROVE_SUBMISSION : Action.ADVISOR_CLEAR_APPROVE_EMBARGO; } - actionLogRepo.createAdvisorPublicLog(submission, approveAdvisorMessage); + ActionLog actionLog = actionLogRepo.createAdvisorPublicLog(action, submission, approveAdvisorMessage); + submissionEmailService.sendActionEmails(submission, actionLog); } private void processAdvisorStatusClear(String type, Boolean approvalState, Submission submission) { String clearAdvisorMessage = "The committee has withdrawn its " + type; + Action action = type.equals("Application") ? Action.ADVISOR_CLEAR_APPROVE_SUBMISSION : Action.ADVISOR_CLEAR_APPROVE_EMBARGO; if (approvalState == true) { clearAdvisorMessage += " Approval."; } else { clearAdvisorMessage += " Rejection."; } - actionLogRepo.createAdvisorPublicLog(submission, clearAdvisorMessage); + ActionLog actionLog = actionLogRepo.createAdvisorPublicLog(action, submission, clearAdvisorMessage); + submissionEmailService.sendActionEmails(submission, actionLog); + } + + private String getName(Submission submission, String predicate) { + List fieldValues = submission.getFieldValuesByPredicateValue(predicate); + + // use Unknown if no field values with predicate or value is empty + String name = fieldValues.isEmpty() || fieldValues.get(0).getValue().trim().isEmpty() + ? ApplicationConstants.UNKNOWN + : fieldValues.get(0).getValue().trim(); + + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + + private void elevateBatchExportException(Exception e) throws BatchExportException { + throw new BatchExportException("Error With Export", e); } } diff --git a/src/main/java/org/tdl/vireo/controller/UserController.java b/src/main/java/org/tdl/vireo/controller/UserController.java index f0aecdef0a..1c805e9743 100644 --- a/src/main/java/org/tdl/vireo/controller/UserController.java +++ b/src/main/java/org/tdl/vireo/controller/UserController.java @@ -158,8 +158,7 @@ public ApiResponse update(@WeaverValidatedModel User updatedUser) { LOG.info("Updating user with email " + persistedUser.getEmail()); persistedUser = userRepo.update(persistedUser); - - userRepo.broadcast(userRepo.findAll()); + LOG.info("Successfully updated user with email " + persistedUser.getEmail()); return new ApiResponse(SUCCESS, persistedUser); } diff --git a/src/main/java/org/tdl/vireo/controller/advice/CustomResponseEntityExceptionHandler.java b/src/main/java/org/tdl/vireo/controller/advice/CustomResponseEntityExceptionHandler.java index 8409345fee..217e83c214 100644 --- a/src/main/java/org/tdl/vireo/controller/advice/CustomResponseEntityExceptionHandler.java +++ b/src/main/java/org/tdl/vireo/controller/advice/CustomResponseEntityExceptionHandler.java @@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.tdl.vireo.exception.BatchExportException; import org.tdl.vireo.exception.SwordDepositBadGatewayException; import org.tdl.vireo.exception.SwordDepositBadRequestException; import org.tdl.vireo.exception.SwordDepositConflictException; @@ -215,6 +216,18 @@ public ApiResponse handleSwordDepositUnprocessableEntityException(SwordDepositEx return ApiResponse.fromException(ERROR, message, exception); } + @ExceptionHandler(BatchExportException.class) + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleBatchExportException(BatchExportException exception) { + String message = exception.getMessage(); + logger.error(message); + logger.debug(message, exception); + + String responseMessage = "The export failed. Check all required metadata and files, then try to export again."; + + return ApiResponse.fromException(ERROR, responseMessage, exception); + } + @ExceptionHandler(Exception.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse handleExceptions(Exception exception) { diff --git a/src/main/java/org/tdl/vireo/exception/BatchExportException.java b/src/main/java/org/tdl/vireo/exception/BatchExportException.java new file mode 100644 index 0000000000..4d673fa5c3 --- /dev/null +++ b/src/main/java/org/tdl/vireo/exception/BatchExportException.java @@ -0,0 +1,13 @@ +package org.tdl.vireo.exception; + +public class BatchExportException extends RuntimeException { + + public BatchExportException(String message) { + super(message); + } + + public BatchExportException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/org/tdl/vireo/model/Action.java b/src/main/java/org/tdl/vireo/model/Action.java new file mode 100644 index 0000000000..d630f2c5ae --- /dev/null +++ b/src/main/java/org/tdl/vireo/model/Action.java @@ -0,0 +1,75 @@ +package org.tdl.vireo.model; + +/** + * Enumeration of action logs. + * + * ActionLogRepoCustom interface offers the following: + * + * create + * create (without user) + * createPublicLog + * createAdvisorPublicLog (without user) + * createPrivateLog + * + * The implementation in ActionLogRepoImpl call the first two create from the subsequent three. + * All other calls to the first two are from tests. + * + * The last three are decomposed below. + * + * createPublicLog + * SubmissionController + * /create......................................................UNDETERMINED + * /{submissionId}/add-comment {commentVisibility: 'public'}....UNDETERMINED + * /batch-comment {commentVisibility: 'public'}.................UNDETERMINED + * /{submissionId}/update-field-value/{fieldProfileId}..........UNDETERMINED + * /{submissionId}/update-custom-action-value...................UNDETERMINED + * /batch-assign-to.............................................UNDETERMINED + * /{submissionId}/submit-date..................................UNDETERMINED + * /{submissionId}/assign-to....................................UNDETERMINED + * /{submissionId}/needs-correction.............................UNDETERMINED + * /{submissionId}/submit-corrections...........................UNDETERMINED + * /{submissionId}/add-message..................................STUDENT_MESSAGE + * /{submissionId}/{documentType}/upload-file...................UNDETERMINED + * /{submissionId}/{documentType}/rename-file...................UNDETERMINED + * /{submissionId}/{fieldValueId}/remove-file...................UNDETERMINED + * /{submissionId}/{documentType}/archive-file..................UNDETERMINED + * + * SubmissionEmailService + * sendAdvisorEmails............................................UNDETERMINED + * SubmissionRepoImpl + * updateStatus.................................................UNDETERMINED + * + * createAdvisorPublicLog (without user) + * SubmissionController + * /{submissionId}/update-advisor-approval + * + * { embargo: { + * approve: boolean,..................................ADVISOR_APPROVE_SUBMISSION + * clearApproval: boolean.............................ADVISOR_CLEAR_APPROVE_SUBMISSION + * }, advisor: { + * approve: boolean...................................ADVISOR_APPROVE_EMBARGO + * clearApproval: boolean.............................ADVISOR_CLEAR_APPROVE_EMBARGO + * }, + * message: string......................................ADVISOR_MESSAGE + * } + * + * createPrivateLog + * SubmissionController + * /{submissionId}/add-comment {commentVisibility: !'public'}...UNDETERMINED + * /batch-comment {commentVisibility: !'public'}................UNDETERMINED + * /{submissionId}/update-reviewer-notes........................UNDETERMINED + * + */ +public enum Action { + STUDENT_MESSAGE, + ADVISOR_MESSAGE, + ADVISOR_APPROVE_SUBMISSION, + ADVISOR_CLEAR_APPROVE_SUBMISSION, + ADVISOR_APPROVE_EMBARGO, + ADVISOR_CLEAR_APPROVE_EMBARGO, + // default value for backfilling existing action logs + // should only be used until a comprehensive enumeration of + // actions is known in the application which should + // be conditioned against email workflow rules by action + UNDETERMINED +} diff --git a/src/main/java/org/tdl/vireo/model/ActionLog.java b/src/main/java/org/tdl/vireo/model/ActionLog.java index 6b401e4f7e..bdc5455abc 100644 --- a/src/main/java/org/tdl/vireo/model/ActionLog.java +++ b/src/main/java/org/tdl/vireo/model/ActionLog.java @@ -1,18 +1,22 @@ package org.tdl.vireo.model; -import java.util.Calendar; - import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.ManyToOne; +import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; -import org.tdl.vireo.model.response.Views; -import org.tdl.vireo.model.validation.ActionLogValidator; +import java.util.Calendar; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonView; +import org.tdl.vireo.model.response.Views; +import org.tdl.vireo.model.validation.ActionLogValidator; import edu.tamu.weaver.validation.model.ValidatingBaseEntity; @@ -21,6 +25,17 @@ */ @Entity @JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" }) +@Table(uniqueConstraints = @UniqueConstraint( + name = "uk_action_log_unique_columns", + columnNames = { + "actionDate", + "action", + "entry", + "privateFlag", + "submission_status_id", + "user_id", + "action_logs_id" +})) public class ActionLog extends ValidatingBaseEntity { @ManyToOne(optional = false) @@ -35,6 +50,11 @@ public class ActionLog extends ValidatingBaseEntity { @Temporal(TemporalType.TIMESTAMP) private Calendar actionDate; + @JsonView(Views.SubmissionList.class) + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Action action; + @JsonView(Views.SubmissionList.class) @Column(nullable = false, columnDefinition = "text") private String entry; @@ -47,16 +67,21 @@ public ActionLog() { setModelValidator(new ActionLogValidator()); } - public ActionLog(SubmissionStatus submissionStatus, Calendar actionDate, String entry, boolean privateFlag) { + public ActionLog(Action action) { this(); + setAction(action); + } + + public ActionLog(Action action, SubmissionStatus submissionStatus, Calendar actionDate, String entry, boolean privateFlag) { + this(action); setSubmissionStatus(submissionStatus); setActionDate(actionDate); setEntry(entry); setPrivateFlag(privateFlag); } - public ActionLog(SubmissionStatus submissionStatus, User user, Calendar actionDate, String entry, boolean privateFlag) { - this(submissionStatus, actionDate, entry, privateFlag); + public ActionLog(Action action, SubmissionStatus submissionStatus, User user, Calendar actionDate, String entry, boolean privateFlag) { + this(action, submissionStatus, actionDate, entry, privateFlag); setUser(user); } @@ -105,6 +130,21 @@ public void setActionDate(Calendar actionDate) { this.actionDate = actionDate; } + /** + * @return the action + */ + public Action getAction() { + return action; + } + + /** + * @param action + * the action to set + */ + public void setAction(Action action) { + this.action = action; + } + /** * @return the entry */ diff --git a/src/main/java/org/tdl/vireo/model/CustomActionValue.java b/src/main/java/org/tdl/vireo/model/CustomActionValue.java index ef43a71209..5f916ad5ef 100644 --- a/src/main/java/org/tdl/vireo/model/CustomActionValue.java +++ b/src/main/java/org/tdl/vireo/model/CustomActionValue.java @@ -25,7 +25,7 @@ public class CustomActionValue extends ValidatingBaseEntity { private CustomActionDefinition definition; @JsonView(Views.SubmissionList.class) - @Column(nullable = false) + @Column(nullable = false, name = "\"value\"") private Boolean value; public CustomActionValue() { diff --git a/src/main/java/org/tdl/vireo/model/EmailRecipientType.java b/src/main/java/org/tdl/vireo/model/EmailRecipientType.java index d4a4782dcf..bb70a90576 100644 --- a/src/main/java/org/tdl/vireo/model/EmailRecipientType.java +++ b/src/main/java/org/tdl/vireo/model/EmailRecipientType.java @@ -1,5 +1,10 @@ package org.tdl.vireo.model; public enum EmailRecipientType { - ASSIGNEE, ADVISOR, CONTACT, ORGANIZATION, PLAIN_ADDRESS, SUBMITTER + /*0*/ ASSIGNEE, + /*1*/ ADVISOR, + /*2*/ CONTACT, + /*3*/ ORGANIZATION, + /*4*/ PLAIN_ADDRESS, + /*5*/ SUBMITTER } diff --git a/src/main/java/org/tdl/vireo/model/EmailWorkflowRule.java b/src/main/java/org/tdl/vireo/model/EmailWorkflowRule.java index b8fa2db233..3798ccf37a 100644 --- a/src/main/java/org/tdl/vireo/model/EmailWorkflowRule.java +++ b/src/main/java/org/tdl/vireo/model/EmailWorkflowRule.java @@ -6,21 +6,17 @@ import static javax.persistence.FetchType.EAGER; import javax.persistence.Column; -import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; -import org.tdl.vireo.model.validation.EmailWorkflowRuleValidator; - -import com.fasterxml.jackson.annotation.JsonIdentityInfo; -import com.fasterxml.jackson.annotation.JsonIdentityReference; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import org.tdl.vireo.model.validation.EmailWorkflowRuleValidator; import edu.tamu.weaver.validation.model.ValidatingBaseEntity; -@Entity -public class EmailWorkflowRule extends ValidatingBaseEntity { +@MappedSuperclass +public abstract class EmailWorkflowRule extends ValidatingBaseEntity { @Column @JsonProperty("isSystem") @@ -33,11 +29,6 @@ public class EmailWorkflowRule extends ValidatingBaseEntity { @ManyToOne(targetEntity = AbstractEmailRecipient.class) private EmailRecipient emailRecipient; - @ManyToOne(fetch = EAGER, optional = false) - @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, scope = SubmissionStatus.class, property = "id") - @JsonIdentityReference(alwaysAsId = true) - private SubmissionStatus submissionStatus; - @ManyToOne(cascade = { DETACH, REFRESH, MERGE }, fetch = EAGER, optional = false) @JoinColumn(name = "emailTemplateId") private EmailTemplate emailTemplate; @@ -48,18 +39,6 @@ public EmailWorkflowRule() { isDisabled(true); } - public EmailWorkflowRule(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate) { - this(); - setSubmissionStatus(submissionStatus); - setEmailRecipient(emailRecipient); - setEmailTemplate(emailTemplate); - } - - public EmailWorkflowRule(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate, Boolean isSystem) { - this(submissionStatus, emailRecipient, emailTemplate); - isSystem(isSystem); - } - /** * @return the isSystem */ @@ -69,7 +48,7 @@ public Boolean isSystem() { /** * @param isSystem - * the isSystem to set + * the isSystem to set */ public void isSystem(Boolean isSystem) { this.isSystem = isSystem; @@ -84,27 +63,12 @@ public Boolean isDisabled() { /** * @param isDisabled - * the isDisabled to set + * the isDisabled to set */ public void isDisabled(Boolean isDisabled) { this.isDisabled = isDisabled; } - /** - * @return the submissionStatus - */ - public SubmissionStatus getSubmissionStatus() { - return submissionStatus; - } - - /** - * @param submissionStatus - * the submissionStatus to set - */ - public void setSubmissionStatus(SubmissionStatus submissionStatus) { - this.submissionStatus = submissionStatus; - } - /** * @return the emailRecipient */ @@ -114,7 +78,7 @@ public EmailRecipient getEmailRecipient() { /** * @param emailRecipient - * the emailRecipient to set + * the emailRecipient to set */ public void setEmailRecipient(EmailRecipient emailRecipient) { this.emailRecipient = emailRecipient; @@ -129,7 +93,7 @@ public EmailTemplate getEmailTemplate() { /** * @param emailTemplate - * the emailTemplate to set + * the emailTemplate to set */ public void setEmailTemplate(EmailTemplate emailTemplate) { this.emailTemplate = emailTemplate; diff --git a/src/main/java/org/tdl/vireo/model/EmailWorkflowRuleByAction.java b/src/main/java/org/tdl/vireo/model/EmailWorkflowRuleByAction.java new file mode 100644 index 0000000000..da7a4c4938 --- /dev/null +++ b/src/main/java/org/tdl/vireo/model/EmailWorkflowRuleByAction.java @@ -0,0 +1,46 @@ +package org.tdl.vireo.model; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; + +@Entity +public class EmailWorkflowRuleByAction extends EmailWorkflowRule { + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Action action; + + public EmailWorkflowRuleByAction() { + super(); + } + + public EmailWorkflowRuleByAction(Action action, EmailRecipient emailRecipient, EmailTemplate emailTemplate) { + this(); + setAction(action); + setEmailRecipient(emailRecipient); + setEmailTemplate(emailTemplate); + } + + public EmailWorkflowRuleByAction(Action action, EmailRecipient emailRecipient, EmailTemplate emailTemplate, boolean isSystem) { + this(action, emailRecipient, emailTemplate); + isSystem(isSystem); + } + + /** + * @return the action + */ + public Action getAction() { + return action; + } + + /** + * @param action + * the action to set + */ + public void setAction(Action action) { + this.action = action; + } + +} diff --git a/src/main/java/org/tdl/vireo/model/EmailWorkflowRuleByStatus.java b/src/main/java/org/tdl/vireo/model/EmailWorkflowRuleByStatus.java new file mode 100644 index 0000000000..0fa05a2102 --- /dev/null +++ b/src/main/java/org/tdl/vireo/model/EmailWorkflowRuleByStatus.java @@ -0,0 +1,53 @@ +package org.tdl.vireo.model; + +import static javax.persistence.FetchType.EAGER; + +import javax.persistence.Entity; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIdentityReference; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; + +@Entity +@Table(name = "email_workflow_rule") +public class EmailWorkflowRuleByStatus extends EmailWorkflowRule { + + @ManyToOne(fetch = EAGER, optional = false) + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, scope = SubmissionStatus.class, property = "id") + @JsonIdentityReference(alwaysAsId = true) + private SubmissionStatus submissionStatus; + + public EmailWorkflowRuleByStatus() { + super(); + } + + public EmailWorkflowRuleByStatus(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate) { + this(); + setSubmissionStatus(submissionStatus); + setEmailRecipient(emailRecipient); + setEmailTemplate(emailTemplate); + } + + public EmailWorkflowRuleByStatus(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate, Boolean isSystem) { + this(submissionStatus, emailRecipient, emailTemplate); + isSystem(isSystem); + } + + /** + * @return the submissionStatus + */ + public SubmissionStatus getSubmissionStatus() { + return submissionStatus; + } + + /** + * @param submissionStatus + * the submissionStatus to set + */ + public void setSubmissionStatus(SubmissionStatus submissionStatus) { + this.submissionStatus = submissionStatus; + } + +} diff --git a/src/main/java/org/tdl/vireo/model/FieldPredicate.java b/src/main/java/org/tdl/vireo/model/FieldPredicate.java index 7f52fa2762..ad618c9c17 100644 --- a/src/main/java/org/tdl/vireo/model/FieldPredicate.java +++ b/src/main/java/org/tdl/vireo/model/FieldPredicate.java @@ -23,7 +23,7 @@ public class FieldPredicate extends ValidatingBaseEntity { private static String period = Pattern.quote("."); @JsonView(ApiView.Partial.class) - @Column(nullable = false, unique = true) + @Column(nullable = false, unique = true, name = "\"value\"") private String value; @JsonView(ApiView.Partial.class) diff --git a/src/main/java/org/tdl/vireo/model/FieldValue.java b/src/main/java/org/tdl/vireo/model/FieldValue.java index 809c6595f8..6342c58b4b 100644 --- a/src/main/java/org/tdl/vireo/model/FieldValue.java +++ b/src/main/java/org/tdl/vireo/model/FieldValue.java @@ -21,7 +21,7 @@ public class FieldValue extends ValidatingBaseEntity { @JsonView(Views.SubmissionList.class) - @Column(columnDefinition = "text", nullable = true) + @Column(columnDefinition = "text", nullable = true, name = "\"value\"") private String value; @JsonView(Views.SubmissionIndividual.class) diff --git a/src/main/java/org/tdl/vireo/model/FilterCriterion.java b/src/main/java/org/tdl/vireo/model/FilterCriterion.java index 01188bb3a9..167fdaa29c 100644 --- a/src/main/java/org/tdl/vireo/model/FilterCriterion.java +++ b/src/main/java/org/tdl/vireo/model/FilterCriterion.java @@ -11,7 +11,7 @@ @Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "value", "gloss" }) }) public class FilterCriterion extends BaseEntity { - @Column + @Column(name = "\"value\"") private String value; @Column diff --git a/src/main/java/org/tdl/vireo/model/GraduationMonth.java b/src/main/java/org/tdl/vireo/model/GraduationMonth.java index ac277fa243..44f89cfb89 100644 --- a/src/main/java/org/tdl/vireo/model/GraduationMonth.java +++ b/src/main/java/org/tdl/vireo/model/GraduationMonth.java @@ -17,7 +17,7 @@ @Entity public class GraduationMonth extends ValidatingOrderedBaseEntity implements EntityControlledVocabulary { - @Column(nullable = false, unique = true) + @Column(nullable = false, unique = true, name = "\"month\"") private int month; public GraduationMonth() { diff --git a/src/main/java/org/tdl/vireo/model/ManagedConfiguration.java b/src/main/java/org/tdl/vireo/model/ManagedConfiguration.java index 8b7a8f4e82..3e10422156 100644 --- a/src/main/java/org/tdl/vireo/model/ManagedConfiguration.java +++ b/src/main/java/org/tdl/vireo/model/ManagedConfiguration.java @@ -23,7 +23,7 @@ public class ManagedConfiguration extends ValidatingBaseEntity implements Config @Column(nullable = false, length = 255) private String name; - @Column(nullable = false, columnDefinition = "text") + @Column(nullable = false, columnDefinition = "text", name = "\"value\"") private String value; @Column(nullable = false, length = 255) diff --git a/src/main/java/org/tdl/vireo/model/Organization.java b/src/main/java/org/tdl/vireo/model/Organization.java index 898065c4c3..d9f40b3e38 100644 --- a/src/main/java/org/tdl/vireo/model/Organization.java +++ b/src/main/java/org/tdl/vireo/model/Organization.java @@ -5,12 +5,6 @@ import static javax.persistence.CascadeType.REMOVE; import static javax.persistence.FetchType.EAGER; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; @@ -23,12 +17,11 @@ import javax.persistence.Transient; import javax.persistence.UniqueConstraint; -import org.hibernate.annotations.Fetch; -import org.hibernate.annotations.FetchMode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.tdl.vireo.model.response.Views; -import org.tdl.vireo.model.validation.OrganizationValidator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; @@ -37,6 +30,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tdl.vireo.model.response.Views; +import org.tdl.vireo.model.validation.OrganizationValidator; import edu.tamu.weaver.validation.model.ValidatingBaseEntity; @@ -88,7 +87,10 @@ public class Organization extends ValidatingBaseEntity { private List emails; @OneToMany(cascade = { REFRESH, MERGE, REMOVE }, fetch = EAGER) - private List emailWorkflowRules; + private List emailWorkflowRules; + + @OneToMany(cascade = { REFRESH, MERGE, REMOVE }, fetch = EAGER) + private List emailWorkflowRulesByAction; /** * Initializer. @@ -100,7 +102,8 @@ public Organization() { setParentOrganization(null); setChildrenOrganizations(new TreeSet()); setEmails(new ArrayList()); - setEmailWorkflowRules(new ArrayList()); + setEmailWorkflowRules(new ArrayList()); + setEmailWorkflowRulesByAction(new ArrayList()); } /** @@ -357,7 +360,7 @@ public Set getChildrenOrganizations() { } /** - * + * * @param childrenOrganizations */ public void setChildrenOrganizations(Set childrenOrganizations) { @@ -417,7 +420,7 @@ public void removeEmail(String email) { /** * @return the emailWorkflowRules */ - public List getEmailWorkflowRules() { + public List getEmailWorkflowRules() { return emailWorkflowRules; } @@ -425,7 +428,7 @@ public List getEmailWorkflowRules() { * @param emailWorkflowRules * the emailWorkflowRules to set */ - public void setEmailWorkflowRules(List emailWorkflowRules) { + public void setEmailWorkflowRules(List emailWorkflowRules) { this.emailWorkflowRules = emailWorkflowRules; } @@ -433,7 +436,7 @@ public void setEmailWorkflowRules(List emailWorkflowRules) { * * @param emailWorkflowRule */ - public void addEmailWorkflowRule(EmailWorkflowRule emailWorkflowRule) { + public void addEmailWorkflowRule(EmailWorkflowRuleByStatus emailWorkflowRule) { getEmailWorkflowRules().add(emailWorkflowRule); } @@ -442,24 +445,101 @@ public void addEmailWorkflowRule(EmailWorkflowRule emailWorkflowRule) { * * @param emailWorkflowRule The workflow rule to remove. */ - public void removeEmailWorkflowRule(EmailWorkflowRule emailWorkflowRule) { + public void removeEmailWorkflowRule(EmailWorkflowRuleByStatus emailWorkflowRule) { getEmailWorkflowRules().remove(emailWorkflowRule); } + /** + * @return the emailWorkflowRulesByAction + */ + public List getEmailWorkflowRulesByAction() { + return emailWorkflowRulesByAction; + } + + /** + * @param emailWorkflowRulesByAction + * the emailWorkflowRulesByAction to set + */ + public void setEmailWorkflowRulesByAction(List emailWorkflowRulesByAction) { + this.emailWorkflowRulesByAction = emailWorkflowRulesByAction; + } + + /** + * + * @param emailWorkflowRuleByAction + */ + public void addEmailWorkflowRuleByAction(EmailWorkflowRuleByAction emailWorkflowRuleByAction) { + getEmailWorkflowRulesByAction().add(emailWorkflowRuleByAction); + } + + /** + * Remove the workflow rule. + * + * @param emailWorkflowRuleByAction The workflow rule to remove. + */ + public void removeEmailWorkflowRuleByAction(EmailWorkflowRuleByAction emailWorkflowRuleByAction) { + getEmailWorkflowRulesByAction().remove(emailWorkflowRuleByAction); + } + @JsonIgnore - public List getAggregateEmailWorkflowRules() { + public List getAggregateEmailWorkflowRules() { - List aggregateEmailWorkflowRules = new ArrayList(); - List newRules = new ArrayList(); + List aggregateEmailWorkflowRules = new ArrayList(); + List newRules = new ArrayList(); aggregateEmailWorkflowRules.addAll(getEmailWorkflowRules()); if (getParentOrganization() != null) { - for (EmailWorkflowRule potentialEmailWorkflowRule : getParentOrganization().getAggregateEmailWorkflowRules()) { - System.out.println("At the ancestor organization " + getParentOrganization().getName() + " considering addition of the rule " + potentialEmailWorkflowRule + " to pass back to caller (org's child or submission controller)"); + for (EmailWorkflowRuleByStatus potentialEmailWorkflowRule : getParentOrganization().getAggregateEmailWorkflowRules()) { + LOG.debug("At the ancestor organization " + getParentOrganization().getName() + " considering addition of the rule " + potentialEmailWorkflowRule + " to pass back to caller (org's child or submission controller)"); + + boolean rejectDuplicateRule = false; + for (EmailWorkflowRuleByStatus currentEmailWorkflowRule : aggregateEmailWorkflowRules) { + String currentEmailRecipientName = ((AbstractEmailRecipient) currentEmailWorkflowRule.getEmailRecipient()).getName(); + String potentialEmailRecipientName = ((AbstractEmailRecipient) potentialEmailWorkflowRule.getEmailRecipient()).getName(); + + String currentEmailTemplateName = currentEmailWorkflowRule.getEmailTemplate().getName(); + String potentialEmailTemplateName = potentialEmailWorkflowRule.getEmailTemplate().getName(); + + LOG.debug("Current email recepient name: " + currentEmailRecipientName); + LOG.debug("Potential email recepient name: " + potentialEmailRecipientName); + + LOG.debug("Current email template name: " + currentEmailTemplateName); + LOG.debug("Potential email template name: " + potentialEmailTemplateName); + + if ((currentEmailRecipientName.equals(potentialEmailRecipientName) & currentEmailTemplateName.equals(potentialEmailTemplateName))) { + LOG.debug("Potential matches a current one for both recipient and template - must reject"); + rejectDuplicateRule = true; + } + } + if (!rejectDuplicateRule) { + LOG.debug("\tThe rule was not a duplicate - adding rule " + potentialEmailWorkflowRule); + newRules.add(potentialEmailWorkflowRule); + } else { + LOG.debug("\tThe rule was a duplicate - ignoring rule " + potentialEmailWorkflowRule); + } + + } + aggregateEmailWorkflowRules.addAll(newRules); + } + + return aggregateEmailWorkflowRules; + } + + @JsonIgnore + public List getAggregateEmailWorkflowRulesByAction() { + + List aggregateEmailWorkflowRules = new ArrayList(); + List newRules = new ArrayList(); + + aggregateEmailWorkflowRules.addAll(getEmailWorkflowRulesByAction()); + + if (getParentOrganization() != null) { + for (EmailWorkflowRuleByAction potentialEmailWorkflowRule : getParentOrganization().getAggregateEmailWorkflowRulesByAction()) { + LOG.debug("At the ancestor organization " + getParentOrganization().getName() + " considering addition of the rule " + potentialEmailWorkflowRule + " to pass back to caller (org's child or submission controller)"); boolean rejectDuplicateRule = false; - for (EmailWorkflowRule currentEmailWorkflowRule : aggregateEmailWorkflowRules) { + for (EmailWorkflowRuleByAction currentEmailWorkflowRule : aggregateEmailWorkflowRules) { String currentEmailRecipientName = ((AbstractEmailRecipient) currentEmailWorkflowRule.getEmailRecipient()).getName(); String potentialEmailRecipientName = ((AbstractEmailRecipient) potentialEmailWorkflowRule.getEmailRecipient()).getName(); diff --git a/src/main/java/org/tdl/vireo/model/User.java b/src/main/java/org/tdl/vireo/model/User.java index 7917b8e0e0..d1a5c61ab7 100644 --- a/src/main/java/org/tdl/vireo/model/User.java +++ b/src/main/java/org/tdl/vireo/model/User.java @@ -85,7 +85,7 @@ public class User extends AbstractWeaverUserDetails { @JsonView(Views.SubmissionIndividual.class) @ElementCollection(fetch = EAGER) @MapKeyColumn(name = "setting") - @Column(name = "value") + @Column(name = "\"value\"") private Map settings; @JsonView(Views.Partial.class) diff --git a/src/main/java/org/tdl/vireo/model/formatter/ProQuestUmiFormatter.java b/src/main/java/org/tdl/vireo/model/formatter/ProQuestUmiFormatter.java index a18594a58f..6109b9070f 100644 --- a/src/main/java/org/tdl/vireo/model/formatter/ProQuestUmiFormatter.java +++ b/src/main/java/org/tdl/vireo/model/formatter/ProQuestUmiFormatter.java @@ -4,6 +4,7 @@ import javax.persistence.Entity; +import org.tdl.vireo.ApplicationConstants; import org.tdl.vireo.model.FieldValue; import org.tdl.vireo.model.Submission; import org.tdl.vireo.model.export.enums.ProQuestUMIKey; @@ -129,11 +130,13 @@ public void populateContext(Context context, Submission submission) { context.setVariable(key.name(), submission.getSupplementalDocumentFieldValues()); break; case PROQUEST_PERSON_FILENAME: - String lastName = submissionHelperUtility.getSubmitterLastName(); + String lastName = submissionHelperUtility.getSubmitterLastName().trim().isEmpty() + ? ApplicationConstants.UNKNOWN : submissionHelperUtility.getSubmitterLastName().trim(); lastName = lastName.substring(0,1).toUpperCase()+lastName.substring(1); - String firstName = submissionHelperUtility.getSubmitterFirstName(); + String firstName = submissionHelperUtility.getSubmitterFirstName().trim().isEmpty() + ? ApplicationConstants.UNKNOWN : submissionHelperUtility.getSubmitterFirstName().trim(); firstName = firstName.substring(0,1).toUpperCase()+firstName.substring(1); - String ufnSuffix = ".pdf"; //default + String ufnSuffix = ".pdf"; // default if(submission.getPrimaryDocumentFieldValue()!=null){ String uploadedFileName = submission.getPrimaryDocumentFieldValue().getFileName(); int ufnIndx; diff --git a/src/main/java/org/tdl/vireo/model/packager/ExcelPackager.java b/src/main/java/org/tdl/vireo/model/packager/ExcelPackager.java index a6a0cf83f1..a897693807 100644 --- a/src/main/java/org/tdl/vireo/model/packager/ExcelPackager.java +++ b/src/main/java/org/tdl/vireo/model/packager/ExcelPackager.java @@ -85,7 +85,7 @@ public ExcelExportPackage packageExport(Submission submission, List, EmailWorkflowRuleRepoCustom { + + public List findByEmailRecipientAndIsDisabled(EmailRecipient emailRecipient, Boolean isDisabled); + + public EmailWorkflowRuleByAction findByActionAndEmailRecipientAndEmailTemplate(Action action, EmailRecipient emailRecipient, EmailTemplate emailTemplate); + +} diff --git a/src/main/java/org/tdl/vireo/model/repo/EmailWorkflowRuleRepo.java b/src/main/java/org/tdl/vireo/model/repo/EmailWorkflowRuleRepo.java index 381895027a..464ebb6f8f 100644 --- a/src/main/java/org/tdl/vireo/model/repo/EmailWorkflowRuleRepo.java +++ b/src/main/java/org/tdl/vireo/model/repo/EmailWorkflowRuleRepo.java @@ -4,16 +4,16 @@ import org.tdl.vireo.model.EmailRecipient; import org.tdl.vireo.model.EmailTemplate; -import org.tdl.vireo.model.EmailWorkflowRule; +import org.tdl.vireo.model.EmailWorkflowRuleByStatus; import org.tdl.vireo.model.SubmissionStatus; import org.tdl.vireo.model.repo.custom.EmailWorkflowRuleRepoCustom; import edu.tamu.weaver.data.model.repo.WeaverRepo; -public interface EmailWorkflowRuleRepo extends WeaverRepo, EmailWorkflowRuleRepoCustom { +public interface EmailWorkflowRuleRepo extends WeaverRepo, EmailWorkflowRuleRepoCustom { - public List findByEmailRecipientAndIsDisabled(EmailRecipient emailRecipient, Boolean isDisabled); + public List findByEmailRecipientAndIsDisabled(EmailRecipient emailRecipient, Boolean isDisabled); - public EmailWorkflowRule findBySubmissionStatusAndEmailRecipientAndEmailTemplate(SubmissionStatus newSubmissionStatus, EmailRecipient emailRecipient, EmailTemplate newEmailTemplate); + public EmailWorkflowRuleByStatus findBySubmissionStatusAndEmailRecipientAndEmailTemplate(SubmissionStatus newSubmissionStatus, EmailRecipient emailRecipient, EmailTemplate newEmailTemplate); } diff --git a/src/main/java/org/tdl/vireo/model/repo/custom/ActionLogRepoCustom.java b/src/main/java/org/tdl/vireo/model/repo/custom/ActionLogRepoCustom.java index 729bcb9ace..7e2ba03fd7 100644 --- a/src/main/java/org/tdl/vireo/model/repo/custom/ActionLogRepoCustom.java +++ b/src/main/java/org/tdl/vireo/model/repo/custom/ActionLogRepoCustom.java @@ -2,20 +2,21 @@ import java.util.Calendar; +import org.tdl.vireo.model.Action; import org.tdl.vireo.model.ActionLog; import org.tdl.vireo.model.Submission; import org.tdl.vireo.model.User; public interface ActionLogRepoCustom { - public ActionLog create(Submission submission, User user, Calendar actionDate, String entry, boolean privateFlag); + public ActionLog create(Action action, Submission submission, User user, Calendar actionDate, String entry, boolean privateFlag); - public ActionLog create(Submission submission, Calendar actionDate, String entry, boolean privateFlag); + public ActionLog create(Action action, Submission submission, Calendar actionDate, String entry, boolean privateFlag); - public ActionLog createPublicLog(Submission submission, User user, String entry); + public ActionLog createPublicLog(Action action, Submission submission, User user, String entry); - public ActionLog createAdvisorPublicLog(Submission submission, String entry); + public ActionLog createAdvisorPublicLog(Action action, Submission submission, String entry); - public ActionLog createPrivateLog(Submission submission, User user, String entry); + public ActionLog createPrivateLog(Action action, Submission submission, User user, String entry); } diff --git a/src/main/java/org/tdl/vireo/model/repo/custom/EmailWorkflowRuleRepoCustom.java b/src/main/java/org/tdl/vireo/model/repo/custom/EmailWorkflowRuleRepoCustom.java index 84391ba5f8..baa879800a 100644 --- a/src/main/java/org/tdl/vireo/model/repo/custom/EmailWorkflowRuleRepoCustom.java +++ b/src/main/java/org/tdl/vireo/model/repo/custom/EmailWorkflowRuleRepoCustom.java @@ -2,13 +2,11 @@ import org.tdl.vireo.model.EmailRecipient; import org.tdl.vireo.model.EmailTemplate; -import org.tdl.vireo.model.EmailWorkflowRule; -import org.tdl.vireo.model.SubmissionStatus; -public interface EmailWorkflowRuleRepoCustom { +public interface EmailWorkflowRuleRepoCustom { - public EmailWorkflowRule create(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate); + public EWR create(SA statusOrAction, EmailRecipient emailRecipient, EmailTemplate emailTemplate); - public EmailWorkflowRule create(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate, Boolean isSystem); + public EWR create(SA statusOrAction, EmailRecipient emailRecipient, EmailTemplate emailTemplate, Boolean isSystem); } diff --git a/src/main/java/org/tdl/vireo/model/repo/impl/ActionLogRepoImpl.java b/src/main/java/org/tdl/vireo/model/repo/impl/ActionLogRepoImpl.java index bb717cff71..a263e10979 100644 --- a/src/main/java/org/tdl/vireo/model/repo/impl/ActionLogRepoImpl.java +++ b/src/main/java/org/tdl/vireo/model/repo/impl/ActionLogRepoImpl.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.tdl.vireo.model.Action; import org.tdl.vireo.model.ActionLog; import org.tdl.vireo.model.Submission; import org.tdl.vireo.model.User; @@ -32,8 +33,8 @@ public class ActionLogRepoImpl extends AbstractWeaverRepoImpl implements EmailWorkflowRuleRepoCustom { + + @Autowired + private EmailWorkflowRuleByActionRepo emailWorkflowRuleRepo; + + @Override + public EmailWorkflowRuleByAction create(Action action, EmailRecipient emailRecipient, EmailTemplate emailTemplate) { + return emailWorkflowRuleRepo.save(new EmailWorkflowRuleByAction(action, emailRecipient, emailTemplate)); + } + + @Override + public EmailWorkflowRuleByAction create(Action action, EmailRecipient emailRecipient, EmailTemplate emailTemplate, Boolean isSystem) { + return emailWorkflowRuleRepo.save(new EmailWorkflowRuleByAction(action, emailRecipient, emailTemplate, isSystem)); + } + + @Override + protected String getChannel() { + return "/channel/embargo-workflow-rule"; + } + +} diff --git a/src/main/java/org/tdl/vireo/model/repo/impl/EmailWorkflowRuleRepoImpl.java b/src/main/java/org/tdl/vireo/model/repo/impl/EmailWorkflowRuleRepoImpl.java index 7fa708953d..1e7e72a644 100644 --- a/src/main/java/org/tdl/vireo/model/repo/impl/EmailWorkflowRuleRepoImpl.java +++ b/src/main/java/org/tdl/vireo/model/repo/impl/EmailWorkflowRuleRepoImpl.java @@ -3,26 +3,26 @@ import org.springframework.beans.factory.annotation.Autowired; import org.tdl.vireo.model.EmailRecipient; import org.tdl.vireo.model.EmailTemplate; -import org.tdl.vireo.model.EmailWorkflowRule; +import org.tdl.vireo.model.EmailWorkflowRuleByStatus; import org.tdl.vireo.model.SubmissionStatus; import org.tdl.vireo.model.repo.EmailWorkflowRuleRepo; import org.tdl.vireo.model.repo.custom.EmailWorkflowRuleRepoCustom; import edu.tamu.weaver.data.model.repo.impl.AbstractWeaverRepoImpl; -public class EmailWorkflowRuleRepoImpl extends AbstractWeaverRepoImpl implements EmailWorkflowRuleRepoCustom { +public class EmailWorkflowRuleRepoImpl extends AbstractWeaverRepoImpl implements EmailWorkflowRuleRepoCustom { @Autowired private EmailWorkflowRuleRepo emailWorkflowRuleRepo; @Override - public EmailWorkflowRule create(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate) { - return emailWorkflowRuleRepo.save(new EmailWorkflowRule(submissionStatus, emailRecipient, emailTemplate)); + public EmailWorkflowRuleByStatus create(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate) { + return emailWorkflowRuleRepo.save(new EmailWorkflowRuleByStatus(submissionStatus, emailRecipient, emailTemplate)); } @Override - public EmailWorkflowRule create(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate, Boolean isSystem) { - return emailWorkflowRuleRepo.save(new EmailWorkflowRule(submissionStatus, emailRecipient, emailTemplate, isSystem)); + public EmailWorkflowRuleByStatus create(SubmissionStatus submissionStatus, EmailRecipient emailRecipient, EmailTemplate emailTemplate, Boolean isSystem) { + return emailWorkflowRuleRepo.save(new EmailWorkflowRuleByStatus(submissionStatus, emailRecipient, emailTemplate, isSystem)); } @Override diff --git a/src/main/java/org/tdl/vireo/model/repo/impl/SubmissionRepoImpl.java b/src/main/java/org/tdl/vireo/model/repo/impl/SubmissionRepoImpl.java index 3d219263b5..48956142a1 100644 --- a/src/main/java/org/tdl/vireo/model/repo/impl/SubmissionRepoImpl.java +++ b/src/main/java/org/tdl/vireo/model/repo/impl/SubmissionRepoImpl.java @@ -41,6 +41,7 @@ import org.tdl.vireo.config.AppFilterConfig; import org.tdl.vireo.config.VireoDatabaseConfig; import org.tdl.vireo.exception.OrganizationDoesNotAcceptSubmissionsException; +import org.tdl.vireo.model.Action; import org.tdl.vireo.model.Configuration; import org.tdl.vireo.model.CustomActionDefinition; import org.tdl.vireo.model.FieldPredicate; @@ -271,7 +272,7 @@ public Submission updateStatus(Submission submission, SubmissionStatus submissio submission = update(submission); simpMessagingTemplate.convertAndSendToUser(user.getUsername(), "/queue/submissions", new ApiResponse(SUCCESS, UPDATE, submission)); - actionLogRepo.createPublicLog(submission, user, "Submission status was changed from " + oldSubmissionStatusName + " to " + submissionStatus.getName()); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Submission status was changed from " + oldSubmissionStatusName + " to " + submissionStatus.getName()); return submission; } diff --git a/src/main/java/org/tdl/vireo/model/repo/specification/AbstractSpecification.java b/src/main/java/org/tdl/vireo/model/repo/specification/AbstractSpecification.java index 51040dbc6d..4b638d2851 100644 --- a/src/main/java/org/tdl/vireo/model/repo/specification/AbstractSpecification.java +++ b/src/main/java/org/tdl/vireo/model/repo/specification/AbstractSpecification.java @@ -36,18 +36,9 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild } } - toPredicateDefaultQueryOrderBy(root, query, cb); - return builder.build(cb); } - /** - * Allow implementing classes to control order by in case lastModified is non-existent. - */ - protected void toPredicateDefaultQueryOrderBy(Root root, CriteriaQuery query, CriteriaBuilder cb) { - query.orderBy(cb.desc(root.get("lastModified"))); - } - private class PredicateBuilder { private final Map> predicates; diff --git a/src/main/java/org/tdl/vireo/model/repo/specification/UserSpecification.java b/src/main/java/org/tdl/vireo/model/repo/specification/UserSpecification.java index 303979f662..bfea71f41a 100644 --- a/src/main/java/org/tdl/vireo/model/repo/specification/UserSpecification.java +++ b/src/main/java/org/tdl/vireo/model/repo/specification/UserSpecification.java @@ -2,10 +2,6 @@ import java.util.Map; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Root; - public class UserSpecification extends AbstractSpecification { private static final long serialVersionUID = 6697995180045425460L; @@ -14,8 +10,4 @@ public UserSpecification(Map filters) { super(filters); } - @Override - protected void toPredicateDefaultQueryOrderBy(Root root, CriteriaQuery query, CriteriaBuilder cb) { - query.orderBy(cb.desc(root.get("id"))); - } } diff --git a/src/main/java/org/tdl/vireo/model/request/FilteredPageRequest.java b/src/main/java/org/tdl/vireo/model/request/FilteredPageRequest.java index b1810ce220..274a9ab4fd 100644 --- a/src/main/java/org/tdl/vireo/model/request/FilteredPageRequest.java +++ b/src/main/java/org/tdl/vireo/model/request/FilteredPageRequest.java @@ -9,6 +9,7 @@ import org.springframework.data.domain.Sort; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.ObjectMapper; import org.tdl.vireo.model.User; import org.tdl.vireo.model.repo.specification.UserSpecification; @@ -39,12 +40,14 @@ public PageRequest getPageRequest() { sort.forEach(sort -> { orders.add(new Sort.Order(sort.getDirection(), sort.getProperty())); }); + orders.add(new Sort.Order(Sort.Direction.ASC, "id")); PageRequest pageRequest; if (orders.isEmpty()) { pageRequest = PageRequest.of(pageNumber > 0 ? pageNumber - 1 : 0, pageSize > 0 ? pageSize : 10); } else { pageRequest = PageRequest.of(pageNumber > 0 ? pageNumber - 1 : 0, pageSize > 0 ? pageSize : 10, Sort.by(orders)); } + return pageRequest; } diff --git a/src/main/java/org/tdl/vireo/service/CliService.java b/src/main/java/org/tdl/vireo/service/CliService.java index c1acce5395..647b894593 100644 --- a/src/main/java/org/tdl/vireo/service/CliService.java +++ b/src/main/java/org/tdl/vireo/service/CliService.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.tdl.vireo.exception.OrganizationDoesNotAcceptSubmissionsException; +import org.tdl.vireo.model.Action; import org.tdl.vireo.model.CustomActionDefinition; import org.tdl.vireo.model.Degree; import org.tdl.vireo.model.Embargo; @@ -393,7 +394,7 @@ private int getRandomNumber(int max) { } private void generateActionLogs(Submission sub, User submitter, boolean expansive, int maxActionLogs) { - actionLogRepo.create(sub, submitter, Calendar.getInstance(), new String("Submission created."), false); + actionLogRepo.create(Action.UNDETERMINED, sub, submitter, Calendar.getInstance(), new String("Submission created."), false); // Only provide large data set when expansive parameter is provided. if (!expansive) { @@ -432,6 +433,7 @@ private void generateActionLogs(Submission sub, User submitter, boolean expansiv if (bySubmitter) { actionLogRepo.create( + Action.UNDETERMINED, sub, submitter, Calendar.getInstance(), @@ -440,6 +442,7 @@ private void generateActionLogs(Submission sub, User submitter, boolean expansiv ); } else { actionLogRepo.create( + Action.UNDETERMINED, sub, Calendar.getInstance(), new String(percent + (random + 1) + " of " + total + (isPrivate ? " [private]" : "") + " [no submitter]."), diff --git a/src/main/java/org/tdl/vireo/service/SubmissionEmailService.java b/src/main/java/org/tdl/vireo/service/SubmissionEmailService.java index d22802eba0..28c725face 100644 --- a/src/main/java/org/tdl/vireo/service/SubmissionEmailService.java +++ b/src/main/java/org/tdl/vireo/service/SubmissionEmailService.java @@ -1,5 +1,7 @@ package org.tdl.vireo.service; +import javax.mail.MessagingException; + import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -8,13 +10,14 @@ import java.util.Map; import java.util.Optional; -import javax.mail.MessagingException; - +import com.fasterxml.jackson.core.JsonProcessingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.stereotype.Service; +import org.tdl.vireo.model.Action; +import org.tdl.vireo.model.ActionLog; import org.tdl.vireo.model.EmailRecipient; import org.tdl.vireo.model.EmailRecipientAssignee; import org.tdl.vireo.model.EmailRecipientContact; @@ -23,20 +26,20 @@ import org.tdl.vireo.model.EmailRecipientSubmitter; import org.tdl.vireo.model.EmailRecipientType; import org.tdl.vireo.model.EmailTemplate; -import org.tdl.vireo.model.EmailWorkflowRule; +import org.tdl.vireo.model.EmailWorkflowRuleByAction; +import org.tdl.vireo.model.EmailWorkflowRuleByStatus; import org.tdl.vireo.model.FieldPredicate; import org.tdl.vireo.model.FieldValue; import org.tdl.vireo.model.Submission; import org.tdl.vireo.model.User; import org.tdl.vireo.model.repo.ActionLogRepo; +import org.tdl.vireo.model.repo.EmailWorkflowRuleByActionRepo; import org.tdl.vireo.model.repo.EmailWorkflowRuleRepo; import org.tdl.vireo.model.repo.FieldPredicateRepo; import org.tdl.vireo.model.repo.SubmissionRepo; import org.tdl.vireo.model.repo.impl.AbstractEmailRecipientRepoImpl; import org.tdl.vireo.utility.TemplateUtility; -import com.fasterxml.jackson.core.JsonProcessingException; - /** * Provide e-mail sending specific to the Submission process. * @@ -59,6 +62,9 @@ public class SubmissionEmailService { @Autowired private EmailWorkflowRuleRepo emailWorkflowRuleRepo; + @Autowired + private EmailWorkflowRuleByActionRepo emailWorkflowRuleByActionRepo; + @Autowired private FieldPredicateRepo fieldPredicateRepo; @@ -80,17 +86,17 @@ public void sendAdvisorEmails(User user, Long submissionId) { Submission submission = submissionRepo.findById(submissionId).get(); EmailRecipient advisorRecipient = abstractEmailRecipientRepoImpl.createAdvisorRecipient(); - List emailWorkflowRules = emailWorkflowRuleRepo.findByEmailRecipientAndIsDisabled(advisorRecipient, false); + List emailWorkflowRules = emailWorkflowRuleRepo.findByEmailRecipientAndIsDisabled(advisorRecipient, false); if (emailWorkflowRules.size() == 0) { - actionLogRepo.createPublicLog(submission, user, "No Advisor email workflow rules are defined and enabled."); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "No Advisor email workflow rules are defined and enabled."); } else { boolean emailed = false; - Iterator emailWorkflowRuleIterator = emailWorkflowRules.iterator(); + Iterator emailWorkflowRuleIterator = emailWorkflowRules.iterator(); while (emailWorkflowRuleIterator.hasNext()) { - EmailWorkflowRule emailWorkflowRule = emailWorkflowRuleIterator.next(); + EmailWorkflowRuleByStatus emailWorkflowRule = emailWorkflowRuleIterator.next(); EmailTemplate template = emailWorkflowRule.getEmailTemplate(); String subject = templateUtility.compileString(template.getSubject(), submission); String content = templateUtility.compileTemplate(template, submission); @@ -119,9 +125,9 @@ public void sendAdvisorEmails(User user, Long submissionId) { } if (emailed) { - actionLogRepo.createPublicLog(submission, user, "Advisor review email manually generated."); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "Advisor review email manually generated."); } else { - actionLogRepo.createPublicLog(submission, user, "No Advisor review emails to generate."); + actionLogRepo.createPublicLog(Action.UNDETERMINED, submission, user, "No Advisor review emails to generate."); } } } @@ -171,7 +177,7 @@ public void sendAutomatedEmails(User user, Long submissionId, Map rules = submission.getOrganization().getAggregateEmailWorkflowRules(); + List rules = submission.getOrganization().getAggregateEmailWorkflowRules(); Map> recipientLists = new HashMap<>(); - for (EmailWorkflowRule rule : rules) { - LOG.debug("Email Workflow Rule " + rule.getId() + " firing for submission " + submission.getId()); - + for (EmailWorkflowRuleByStatus rule : rules) { if (rule.getSubmissionStatus().equals(submission.getSubmissionStatus()) && !rule.isDisabled()) { + LOG.debug("Email Workflow Rule " + rule.getId() + " firing for submission " + submission.getId()); Long templateId = rule.getEmailTemplate().getId(); if (!recipientLists.containsKey(templateId)) { @@ -231,6 +236,72 @@ public void sendWorkflowEmails(User user, Long submissionId) { } } + /** + * Send any emails for given submission and action log. + * + * Check recursively the submission organization for email workflow rules by action. + * + * @param submission Submission. + * @param actionLog ActionLog. + */ + public void sendActionEmails(Submission submission, ActionLog actionLog) { + Action action = actionLog.getAction(); + + switch (action) { + case STUDENT_MESSAGE: + case ADVISOR_MESSAGE: + case ADVISOR_APPROVE_SUBMISSION: + case ADVISOR_CLEAR_APPROVE_SUBMISSION: + case ADVISOR_APPROVE_EMBARGO: + case ADVISOR_CLEAR_APPROVE_EMBARGO: + processSubmissionActionLog(submission, actionLog); + LOG.info("Submission {}: {} action logged processed", submission.getId(), action); + break; + case UNDETERMINED: + default: + break; + } + + } + + private void processSubmissionActionLog(Submission submission, ActionLog actionLog) { + List rules = submission.getOrganization().getAggregateEmailWorkflowRulesByAction(); + Map> recipientLists = new HashMap<>(); + + for (EmailWorkflowRuleByAction rule : rules) { + if (rule.getAction().equals(actionLog.getAction()) && !rule.isDisabled()) { + LOG.info("Action Email Workflow Rule " + rule.getId() + " firing for submission " + submission.getId()); + Long templateId = rule.getEmailTemplate().getId(); + + if (!recipientLists.containsKey(templateId)) { + recipientLists.put(templateId, new ArrayList<>()); + } + + // TODO: Not all variables are currently being replaced. + String subject = templateUtility.compileString(rule.getEmailTemplate().getSubject(), submission); + String content = templateUtility.compileTemplate(rule.getEmailTemplate(), submission); + + for (String email : rule.getEmailRecipient().getEmails(submission)) { + if (recipientLists.get(templateId).contains(email)) { + LOG.debug("\tSkipping (already sent) email to recipient at address " + email); + continue; + } + + recipientLists.get(templateId).add(email); + + try { + emailSender.sendEmail(email, subject, content); + } catch (MessagingException me) { + LOG.error("Problem sending email: " + me.getMessage()); + recipientLists.get(templateId).remove(email); + } + } + } else { + LOG.debug("\tRule disabled or of irrelevant status condition."); + } + } + } + /** * Build the e-mail recipient list. * @@ -278,16 +349,16 @@ private EmailRecipient buildEmailRecipient(HashMap emailRecipien case ADVISOR: { String label = (String) emailRecipientMap.get("name"); FieldPredicate fp = fieldPredicateRepo.findByValue("dc.contributor.advisor"); - if (label != null & fp != null) { + if (label != null && fp != null) { recipient = new EmailRecipientContact(label, fp); } break; } case CONTACT: { String label = (String) emailRecipientMap.get("name"); - + Optional fp = fieldPredicateRepo.findById(Long.valueOf((int) emailRecipientMap.get("data"))); - if (label != null & fp.isPresent()) { + if (label != null && fp.isPresent()) { recipient = new EmailRecipientContact(label, fp.get()); } break; diff --git a/src/main/java/org/tdl/vireo/service/SystemDataLoader.java b/src/main/java/org/tdl/vireo/service/SystemDataLoader.java index 60c108ff27..82b3ee89b1 100644 --- a/src/main/java/org/tdl/vireo/service/SystemDataLoader.java +++ b/src/main/java/org/tdl/vireo/service/SystemDataLoader.java @@ -9,6 +9,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,13 +22,15 @@ import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.tdl.vireo.model.Action; import org.tdl.vireo.model.ControlledVocabulary; import org.tdl.vireo.model.Degree; import org.tdl.vireo.model.DegreeLevel; import org.tdl.vireo.model.DocumentType; import org.tdl.vireo.model.EmailRecipient; import org.tdl.vireo.model.EmailTemplate; -import org.tdl.vireo.model.EmailWorkflowRule; +import org.tdl.vireo.model.EmailWorkflowRuleByAction; +import org.tdl.vireo.model.EmailWorkflowRuleByStatus; import org.tdl.vireo.model.Embargo; import org.tdl.vireo.model.FieldPredicate; import org.tdl.vireo.model.FieldProfile; @@ -55,6 +61,7 @@ import org.tdl.vireo.model.repo.DegreeRepo; import org.tdl.vireo.model.repo.DocumentTypeRepo; import org.tdl.vireo.model.repo.EmailTemplateRepo; +import org.tdl.vireo.model.repo.EmailWorkflowRuleByActionRepo; import org.tdl.vireo.model.repo.EmailWorkflowRuleRepo; import org.tdl.vireo.model.repo.EmbargoRepo; import org.tdl.vireo.model.repo.FieldPredicateRepo; @@ -70,11 +77,6 @@ import org.tdl.vireo.model.repo.VocabularyWordRepo; import org.tdl.vireo.model.repo.WorkflowStepRepo; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; - /** * This class is to load and persist system data from resources. */ @@ -151,6 +153,9 @@ public class SystemDataLoader { @Autowired private EmailWorkflowRuleRepo emailWorkflowRuleRepo; + @Autowired + private EmailWorkflowRuleByActionRepo emailWorkflowRuleByActionRepo; + @Autowired private SubmissionStatusRepo submissionStatusRepo; @@ -655,10 +660,19 @@ private void processWorkflowSteps(Organization organization, Organization system } } + /** + * @param organization + * @param systemOrganization + */ private void processEmailWorflowRules(Organization organization, Organization systemOrganization) { - List emailWorkflowRules = organization.getEmailWorkflowRules(); + processEmailWorkflowRulesByStatus(organization, systemOrganization); + processEmailWorkflowRulesByAction(organization, systemOrganization); + } + + private void processEmailWorkflowRulesByStatus(Organization organization, Organization systemOrganization) { + List emailWorkflowRules = organization.getEmailWorkflowRules(); - for (EmailWorkflowRule emailWorkflowRule : systemOrganization.getEmailWorkflowRules()) { + for (EmailWorkflowRuleByStatus emailWorkflowRule : systemOrganization.getEmailWorkflowRules()) { // check to see if the SubmissionStatus exists SubmissionStatus newSubmissionStatus = submissionStatusRepo.findByName(emailWorkflowRule.getSubmissionStatus().getName()); @@ -678,6 +692,8 @@ private void processEmailWorflowRules(Organization organization, Organization sy if (emailWorkflowRule.getEmailRecipient() == null) { + // NOTE: this is a mapping directly to the SYSTEM_Organization_Definition.json for `emailWorkflowRules` property. + if (newEmailTemplate.getName().equals("SYSTEM Advisor Review Request")) { organization.getAggregateWorkflowSteps().forEach(awfs -> { awfs.getAggregateFieldProfiles().forEach(afp -> { @@ -687,7 +703,6 @@ private void processEmailWorflowRules(Organization organization, Organization sy } }); }); - } if (newEmailTemplate.getName().equals("SYSTEM Initial Submission")) { @@ -698,7 +713,7 @@ private void processEmailWorflowRules(Organization organization, Organization sy } // check to see if the EmailWorkflowRule exists - EmailWorkflowRule existingEmailWorkflowRule = emailWorkflowRuleRepo.findBySubmissionStatusAndEmailRecipientAndEmailTemplate(newSubmissionStatus, emailWorkflowRule.getEmailRecipient(), newEmailTemplate); + EmailWorkflowRuleByStatus existingEmailWorkflowRule = emailWorkflowRuleRepo.findBySubmissionStatusAndEmailRecipientAndEmailTemplate(newSubmissionStatus, emailWorkflowRule.getEmailRecipient(), newEmailTemplate); if (existingEmailWorkflowRule == null) { emailWorkflowRules.add(emailWorkflowRuleRepo.create(newSubmissionStatus, emailWorkflowRule.getEmailRecipient(), newEmailTemplate, emailWorkflowRule.isSystem())); @@ -709,6 +724,79 @@ private void processEmailWorflowRules(Organization organization, Organization sy organization.setEmailWorkflowRules(emailWorkflowRules); } + // TODO: fix duplicate method with different typing for repo and entity + // NOTE: applies same dependent persistent email recipient creation in both + private void processEmailWorkflowRulesByAction(Organization organization, Organization systemOrganization) { + List emailWorkflowRulesByAction = organization.getEmailWorkflowRulesByAction(); + + EmailRecipient submitterRecipient = abstractEmailRecipientRepo.createSubmitterRecipient(); + + for (EmailWorkflowRuleByAction emailWorkflowRuleByAction : systemOrganization.getEmailWorkflowRulesByAction()) { + + // check to see if the action is defined + Action action = emailWorkflowRuleByAction.getAction(); + try { + logger.info("Checking if action is defined: {}", action.name()); + } catch (Exception e) { + throw new RuntimeException(String.format("Action is required on EmailWorfklowRuleByAction %s", emailWorkflowRuleByAction), e); + } + + // check to see if the EmailTemplate exists + // see ../model/EmailTemplate.java => { "name", "systemRequired" } + EmailTemplate newEmailTemplate = emailTemplateRepo.findByNameAndSystemRequired(emailWorkflowRuleByAction.getEmailTemplate().getName(), emailWorkflowRuleByAction.getEmailTemplate().getSystemRequired()); + + // create new EmailTemplate if not already exists + if (newEmailTemplate == null) { + newEmailTemplate = emailTemplateRepo.create(emailWorkflowRuleByAction.getEmailTemplate().getName(), emailWorkflowRuleByAction.getEmailTemplate().getSubject(), emailWorkflowRuleByAction.getEmailTemplate().getMessage()); + } + + if (emailWorkflowRuleByAction.getEmailRecipient() == null) { + + // NOTE: this is a mapping directly to the SYSTEM_Organization_Definition.json for `emailWorkflowRulesByAction` property. + + switch (newEmailTemplate.getName()) { + case "SYSTEM Notify Assignee of Student Comment": + case "SYSTEM Notify Assignee of Advisor Comment": + case "SYSTEM Notify Assignee of Advisor Approved Submission": + case "SYSTEM Notify Assignee of Advisor Cleared Submission Approval": + case "SYSTEM Notify Assignee of Advisor Approved Embargo": + case "SYSTEM Notify Assignee of Advisor Cleared Embargo Approval": { + EmailRecipient recipient = abstractEmailRecipientRepo.createOrganizationRecipient(organization); + emailWorkflowRuleByAction.setEmailRecipient(recipient); + } break; + case "SYSTEM Notify Advisor of Student Comment": { + organization.getAggregateWorkflowSteps().forEach(awfs -> { + awfs.getAggregateFieldProfiles().forEach(afp -> { + if (afp.getFieldPredicate().getValue().equals("dc.contributor.advisor")) { + EmailRecipient recipient = abstractEmailRecipientRepo.createContactRecipient(afp.getGloss(), afp.getFieldPredicate()); + emailWorkflowRuleByAction.setEmailRecipient(recipient); + } + }); + }); + } break; + case "SYSTEM Notify Submitter of Advisor Comment": + case "SYSTEM Notify Submitter of Advisor Approved Submission": + case "SYSTEM Notify Submitter of Advisor Cleared Submission Approval": + case "SYSTEM Notify Submitter of Advisor Approved Embargo": + case "SYSTEM Notify Submitter of Advisor Cleared Embargo Approval": + default: + emailWorkflowRuleByAction.setEmailRecipient(submitterRecipient); + break; + } + } + + // check to see if the EmailWorkflowRule exists + EmailWorkflowRuleByAction existingEmailWorkflowRule = emailWorkflowRuleByActionRepo.findByActionAndEmailRecipientAndEmailTemplate(action, emailWorkflowRuleByAction.getEmailRecipient(), newEmailTemplate); + + if (existingEmailWorkflowRule == null) { + emailWorkflowRulesByAction.add(emailWorkflowRuleByActionRepo.create(action, emailWorkflowRuleByAction.getEmailRecipient(), newEmailTemplate, emailWorkflowRuleByAction.isSystem())); + } + + } + + organization.setEmailWorkflowRulesByAction(emailWorkflowRulesByAction); + } + private void loadControlledVocabularies() throws IOException { for (Resource vocabularyResourceJson : resourcePatternResolver.getResources("classpath:/controlled_vocabularies/*.json")) { ControlledVocabulary cv = objectMapper.readValue(vocabularyResourceJson.getInputStream(), ControlledVocabulary.class); diff --git a/src/main/java/org/tdl/vireo/utility/FormatterUtility.java b/src/main/java/org/tdl/vireo/utility/FormatterUtility.java index c97ff55f15..9e7e143b02 100644 --- a/src/main/java/org/tdl/vireo/utility/FormatterUtility.java +++ b/src/main/java/org/tdl/vireo/utility/FormatterUtility.java @@ -6,7 +6,6 @@ import javax.annotation.Resource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.tdl.vireo.model.Submission; import org.tdl.vireo.model.formatter.Formatter; @@ -20,13 +19,16 @@ public class FormatterUtility { @Resource(name = "templateResolver") private SpringResourceTemplateResolver resolver; - @Autowired private SpringTemplateEngine templateEngine; - public Map renderManifestMap(Formatter formatter, Submission submission) throws Exception { + public FormatterUtility(SpringTemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + public Map renderManifestMap(Formatter formatter, Submission submission) { resolver.setSuffix(formatter.getSuffix()); resolver.setTemplateMode(formatter.getTemplateMode()); - Map renderMap = new HashMap(); + Map renderMap = new HashMap<>(); Context context = new Context(Locale.getDefault()); formatter.populateContext(context, submission); Map templates = formatter.getTemplates(); diff --git a/src/main/java/org/tdl/vireo/utility/PackagerUtility.java b/src/main/java/org/tdl/vireo/utility/PackagerUtility.java index c59d8a36f7..fcbf3c0fbc 100644 --- a/src/main/java/org/tdl/vireo/utility/PackagerUtility.java +++ b/src/main/java/org/tdl/vireo/utility/PackagerUtility.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.Map; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.tdl.vireo.exception.UnsupportedFormatterException; import org.tdl.vireo.model.Submission; @@ -16,15 +15,21 @@ @Service public class PackagerUtility { - @Autowired + private static final String TEMPLATE_KEY = "template"; + private AbstractPackagerRepo abstractPackagerRepo; - @Autowired private FormatterUtility formatterUtility; - private String TEMPLATE_KEY = "template"; + public PackagerUtility( + AbstractPackagerRepo abstractPackagerRepo, + FormatterUtility formatterUtility + ) { + this.abstractPackagerRepo = abstractPackagerRepo; + this.formatterUtility = formatterUtility; + } - public ExportPackage packageExport(Packager packager, Submission submission) throws Exception { + public ExportPackage packageExport(Packager packager, Submission submission) { Map formatterMap = formatterUtility.renderManifestMap(packager.getFormatter(), submission); if (formatterMap.isEmpty()) { throw new UnsupportedFormatterException("Required manifest not found!"); diff --git a/src/main/java/org/tdl/vireo/utility/SubmissionHelperUtility.java b/src/main/java/org/tdl/vireo/utility/SubmissionHelperUtility.java index 6a2d5c2e11..9bf60cb99c 100644 --- a/src/main/java/org/tdl/vireo/utility/SubmissionHelperUtility.java +++ b/src/main/java/org/tdl/vireo/utility/SubmissionHelperUtility.java @@ -1,5 +1,7 @@ package org.tdl.vireo.utility; +import static java.lang.String.format; + import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -26,9 +28,13 @@ import edu.tamu.weaver.context.SpringContext; import org.apache.commons.lang3.time.DateUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SubmissionHelperUtility { + private static final Logger logger = LoggerFactory.getLogger(SubmissionHelperUtility.class); + private final static SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); private final static SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy"); @@ -124,13 +130,13 @@ public Submission getSubmission() { /** * Scott Phillips's address parsing algorithm. - * + * * This algorithm works by searching backwards. Starting at the end of the address identify the zip code. Once you have that assume anything following the zip code is the country, and then the two tokens preceding the zip code are the city and state. We do this by performing a series of regular expressions on a reverse address string. The difference between the regular expressions is how linent they are for extracting the city and state. The first one in the list demands that city and state * are either separated by a new line or a comma. Each of the next versions back off of this by allowing spaces between these tokens. This sometimes breaks multi-word cities or state, but sometimes people just don't supply a city. - * + * * This algorithm works on most international and American addresses. However it will sometimes miss identify components like getting the city or state wrong. It will often not identify the country if it is specified before the zip code. - * - * + * + * * @param fullAddress * The full address * @return An address object if the parse was successful, otherwise return null. @@ -280,13 +286,15 @@ public String getGraduationDateString() { if (graduationDate.isPresent()) { try { date = dateFormat.format(monthYearFormat.parse(graduationDate.get())); - } catch (ParseException e) { - e.printStackTrace(); + } catch (NumberFormatException | ParseException e) { + logException(e, format("Failed to format graduation date %s for submission with id %s", graduationDate.get(), submission.getId())); } } return date; } + + // NOTE: uses hard coded predicate values public String getGraduationYearString() { Optional graduationYear = getFieldValueByPredicateValue("dc.date.issued"); @@ -294,8 +302,8 @@ public String getGraduationYearString() { if (graduationYear.isPresent()) { try { year = yearFormat.format(monthYearFormat.parse(graduationYear.get())); - } catch (ParseException e) { - e.printStackTrace(); + } catch (NumberFormatException | ParseException e) { + logException(e, format("Failed to format graduation year %s for submission with id %s", graduationYear.get(), submission.getId())); } } return year; @@ -308,8 +316,8 @@ public String getGraduationYearMonthString() { if (graduationYearMonth.isPresent()) { try { yearMonth = yearMonthFormat.format(monthYearFormat.parse(graduationYearMonth.get())); - } catch (ParseException e) { - e.printStackTrace(); + } catch (NumberFormatException | ParseException e) { + logException(e, format("Failed to format graduation year month %s for submission with id %s", graduationYearMonth.get(), submission.getId())); } } return yearMonth; @@ -400,7 +408,7 @@ public String getCountryCode(String number) { phoneNumber = phoneUtil.parse(number, "US"); code = String.valueOf(phoneNumber.getCountryCode()); } catch (NumberParseException e) { - e.printStackTrace(); + logException(e, format("Failed to parse country code from phone number %s for submission with id %s", number, submission.getId())); } return code; } @@ -415,7 +423,7 @@ public String getAreaCode(String number) { areaCode = Optional.of(nationalSignificantNumber.substring(0, areaCodeLength)); } } catch (NumberParseException e) { - e.printStackTrace(); + logException(e, format("Failed to parse area code from phone number %s for submission with id %s", number, submission.getId())); } return areaCode.isPresent() ? areaCode.get() : ""; } @@ -429,7 +437,7 @@ public String getNumber(String number) { fullNumber = fullNumber.substring(3, fullNumber.length()); } } catch (NumberParseException e) { - e.printStackTrace(); + logException(e, format("Failed to parse phone number %s for submission with id %s", number, submission.getId())); } return fullNumber; @@ -441,7 +449,7 @@ public String getExt(String number) { PhoneNumber phoneNumber = phoneUtil.parse(number, "US"); ext = phoneNumber.getExtension(); } catch (NumberParseException e) { - e.printStackTrace(); + logException(e, format("Failed to parse extension for phone number %s for submission with id %s", number, submission.getId())); } return ext; } @@ -623,7 +631,7 @@ public int getEmbargoCode() { // NOTE: these come from the settings service - public String getGrantor() { + public String getGrantor() { return getSettingByNameAndType("grantor","application").getValue(); } @@ -673,7 +681,7 @@ public String getEmbargoLiftDate() { Boolean proquestEmbargoCheck = proquestEmbargo.isPresent() && proquestEmbargo.get().getIdentifier() != null && Integer.valueOf(proquestEmbargo.get().getIdentifier()) > 0; Boolean defaultEmbargoCheck = defaultEmbargo.isPresent() && defaultEmbargo.get().getIdentifier() != null && Integer.valueOf(defaultEmbargo.get().getIdentifier()) > 0; - + if (proquestEmbargoCheck && defaultEmbargoCheck) { embargo = Integer.valueOf(proquestEmbargo.get().getIdentifier()) >= Integer.valueOf(defaultEmbargo.get().getIdentifier()) ? proquestEmbargo @@ -692,13 +700,13 @@ public String getEmbargoLiftDate() { java.util.Date embargoLiftDate = DateUtils.addMonths(monthYearFormat.parse(dateIssuedStr), duration); embargoLiftDateStr = iso8601Format.format(embargoLiftDate); } catch (ParseException e) { - e.printStackTrace(); + logException(e, format("Failed to format embargo lift date from duration %s for submission with id %s", duration, submission.getId())); } } } } return embargoLiftDateStr; - } + } public String getProQuestFormatRestrictionRemove() { String proquestLiftDateStr = ""; @@ -714,7 +722,7 @@ public String getProQuestFormatRestrictionRemove() { java.util.Date proquestLiftDate = DateUtils.addMonths(monthYearFormat.parse(dateIssuedStr),d); proquestLiftDateStr = dateFormat.format(proquestLiftDate); } catch (ParseException e) { - e.printStackTrace(); + logException(e, format("Failed to format proquest lift date from duration %s for submission with id %s", d, submission.getId())); } } } @@ -748,4 +756,12 @@ public Optional getProQuestCodeByNameAndType(String name, String type) { return code; } + private void logException(Exception e, String message) { + if (logger.isDebugEnabled()) { + logger.debug(message, e); + } else { + logger.info(message); + } + } + } diff --git a/src/main/java/org/tdl/vireo/view/ShallowOrganizationView.java b/src/main/java/org/tdl/vireo/view/ShallowOrganizationView.java index dea6b2ea9e..989fb3a9e8 100644 --- a/src/main/java/org/tdl/vireo/view/ShallowOrganizationView.java +++ b/src/main/java/org/tdl/vireo/view/ShallowOrganizationView.java @@ -1,11 +1,13 @@ package org.tdl.vireo.view; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.ObjectIdGenerators; -import java.util.List; -import org.tdl.vireo.model.EmailWorkflowRule; +import org.tdl.vireo.model.EmailWorkflowRuleByAction; +import org.tdl.vireo.model.EmailWorkflowRuleByStatus; import org.tdl.vireo.model.OrganizationCategory; public interface ShallowOrganizationView extends TreeOrganizationView { @@ -22,6 +24,8 @@ public interface ShallowOrganizationView extends TreeOrganizationView { public List getEmails(); - public List getEmailWorkflowRules(); + public List getEmailWorkflowRules(); + + public List getEmailWorkflowRulesByAction(); } diff --git a/src/main/resources/emails/SYSTEM_Advisor_Review_Request.email b/src/main/resources/emails/SYSTEM_Advisor_Review_Request.email index 5b4ff3b1f1..7db2606e86 100644 --- a/src/main/resources/emails/SYSTEM_Advisor_Review_Request.email +++ b/src/main/resources/emails/SYSTEM_Advisor_Review_Request.email @@ -1,11 +1,12 @@ -# Email sent to students have they have completed the submittal process. +# Email sent to students confirming they have completed the submittal process. # # Paramaters: # {FULL_NAME}: The student's full, official name # {ADVISOR_URL}: A URL where the advisor may review the submission. -# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. # {SUBMISSION_TYPE}: The document type, typically 'Thesis' or 'Dissertation' # {SUBMISSION_STATUS}: The current status of the submissions, e.g. 'Approved' or 'Needs Corrections' +# {SUBMISSION_ASSIGNED_TO}: The name of the Assignee for the submission # Subject: Review: {FULL_NAME}'s {SUBMISSION_TYPE} submission Dear Committee Chair: diff --git a/src/main/resources/emails/SYSTEM_Deposit_Notification.email b/src/main/resources/emails/SYSTEM_Deposit_Notification.email index db3403bad2..8e747a0572 100644 --- a/src/main/resources/emails/SYSTEM_Deposit_Notification.email +++ b/src/main/resources/emails/SYSTEM_Deposit_Notification.email @@ -1,6 +1,9 @@ -# Email sent to Vireo users to verify deposit into the IR +# Email sent to Vireo users to verify deposit into the IR. # # Parameters: {DEPOSIT_URI} +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# {FULL_NAME}: The student's full, official name +# {DEPOSIT_URI}: A URL where the advisor may review the submission. # Subject: Deposit Notification diff --git a/src/main/resources/emails/SYSTEM_Notify_Advisor_of_Student_Comment.email b/src/main/resources/emails/SYSTEM_Notify_Advisor_of_Student_Comment.email new file mode 100644 index 0000000000..caea80eeac --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Advisor_of_Student_Comment.email @@ -0,0 +1,18 @@ +# Email sent notifying Advisor of message left by Student. +# +# Parameters: +# {FULL_NAME}: The student's full, official name +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# {ADVISOR_URL}: A URL where the advisor may review the submission. +# +Subject: Notify Advisor of Student Comment + +A message has been sent by {FULL_NAME} concerning the following submission: + +Title: {DOCUMENT_TITLE} + +Please see the action log for more information. + + {ADVISOR_URL} + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Approved_Embargo.email b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Approved_Embargo.email new file mode 100644 index 0000000000..3b7eed8ad7 --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Approved_Embargo.email @@ -0,0 +1,14 @@ +# Email sent notifying Assignee of submission embargo approval by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Assignee of Advisor Approved Embargo + +The advisor has approved the embargo for the following submission: + +Title: {DOCUMENT_TITLE} + +Please see the action log for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Approved_Submission.email b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Approved_Submission.email new file mode 100644 index 0000000000..b02a14f9ae --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Approved_Submission.email @@ -0,0 +1,14 @@ +# Email sent notifying Assignee of submission approval by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Assignee of Advisor Approved Submission + +The advisor has approved the embargo for the following submission: + +Title: {DOCUMENT_TITLE} + +Please see the action log for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Cleared_Embargo_Approval.email b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Cleared_Embargo_Approval.email new file mode 100644 index 0000000000..b0eb88219b --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Cleared_Embargo_Approval.email @@ -0,0 +1,14 @@ +# Email sent notifying Assignee of submission embargo approval withdrawn by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Assignee of Advisor Embargo Approval Withdrawn + +The advisor has withdrawn embargo approval the following submission: + +Title: {DOCUMENT_TITLE} + +Please see the action log for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Cleared_Submission_Approval.email b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Cleared_Submission_Approval.email new file mode 100644 index 0000000000..c084529ed2 --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Cleared_Submission_Approval.email @@ -0,0 +1,14 @@ +# Email sent notifying Assignee of submission approval withdrawn by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Assignee of Advisor Submission Approval Withdrawn + +The advisor has withdrawn approval of the following submission: + +Title: {DOCUMENT_TITLE} + +Please see the action log for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Comment.email b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Comment.email new file mode 100644 index 0000000000..dea496b14c --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Advisor_Comment.email @@ -0,0 +1,14 @@ +# Email sent notifying Assignee of message sent by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Submitter of Advisor Comment + +The advisor has sent a message concerning the following submission: + +Title: {DOCUMENT_TITLE} + +Please see the action log for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Student_Comment.email b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Student_Comment.email new file mode 100644 index 0000000000..ba71e6883e --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Assignee_of_Student_Comment.email @@ -0,0 +1,15 @@ +# Email sent notifying Assignee of message sent by Student. +# +# Parameters: +# {FULL_NAME}: The student's full, official name +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Assignee of Student Comment + +{FULL_NAME} has sent a message concerning the following submission: + +Title: {DOCUMENT_TITLE} + +Please see the action log for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Approved_Embargo.email b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Approved_Embargo.email new file mode 100644 index 0000000000..99652d9d62 --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Approved_Embargo.email @@ -0,0 +1,12 @@ +# Email sent notifying Submitter of embargo approval by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Submitter of Advisor Approved Embargo + +Your advisor has approved the embargo for the following submission: + +Title: {DOCUMENT_TITLE} + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Approved_Submission.email b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Approved_Submission.email new file mode 100644 index 0000000000..556439755c --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Approved_Submission.email @@ -0,0 +1,12 @@ +# Email sent notifying Submitter of submission approval by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Submitter of Advisor Approved Submission + +Your advisor has approved the following submission: + +Title: {DOCUMENT_TITLE} + +The Vireo Team \ No newline at end of file diff --git a/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Cleared_Embargo_Approval.email b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Cleared_Embargo_Approval.email new file mode 100644 index 0000000000..b40004b60a --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Cleared_Embargo_Approval.email @@ -0,0 +1,14 @@ +# Email sent notifying Submitter of embargo approval withdrawn by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Submitter of Advisor Embargo Approval Withdrawn + +Your advisor has withdrawn embargo approval of the following submission: + +Title: {DOCUMENT_TITLE} + +Please login to your account for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Cleared_Submission_Approval.email b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Cleared_Submission_Approval.email new file mode 100644 index 0000000000..8dd04eac34 --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Cleared_Submission_Approval.email @@ -0,0 +1,14 @@ +# Email sent notifying Submitter of submission approval withdrawn by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Submitter of Advisor Submission Approval Withdrawn + +Your advisor has withdrawn approval of the following submission: + +Title: {DOCUMENT_TITLE} + +Please login to your account for more information. + +The Vireo Team diff --git a/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Comment.email b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Comment.email new file mode 100644 index 0000000000..452c13dd4c --- /dev/null +++ b/src/main/resources/emails/SYSTEM_Notify_Submitter_of_Advisor_Comment.email @@ -0,0 +1,14 @@ +# Email sent notifying Submitter of message sent by Advisor. +# +# Parameters: +# {DOCUMENT_TITLE}: The title of the thesis or dissertation as supplied by the student. +# +Subject: Notify Assignee of Advisor Comment + +Your advisor has sent a message concerning the following submission: + +Title: {DOCUMENT_TITLE} + +Please login to your account for more information. + +The Vireo Team diff --git a/src/main/resources/organization/SYSTEM_Organization_Definition.json b/src/main/resources/organization/SYSTEM_Organization_Definition.json index 01fa5f62fe..6b2d8398f0 100644 --- a/src/main/resources/organization/SYSTEM_Organization_Definition.json +++ b/src/main/resources/organization/SYSTEM_Organization_Definition.json @@ -1053,5 +1053,127 @@ "systemRequired": true } } + ], + "emailWorkflowRulesByAction": [ + { + "isSystem": true, + "isDisabled": true, + "action": "STUDENT_MESSAGE", + "recipientType": 0, + "emailTemplate": { + "name": "SYSTEM Notify Assignee of Student Comment", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "STUDENT_MESSAGE", + "recipientType": 1, + "emailTemplate": { + "name": "SYSTEM Notify Advisor of Student Comment", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_MESSAGE", + "recipientType": 5, + "emailTemplate": { + "name": "SYSTEM Notify Submitter of Advisor Comment", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_MESSAGE", + "recipientType": 0, + "emailTemplate": { + "name": "SYSTEM Notify Assignee of Advisor Comment", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_APPROVE_SUBMISSION", + "recipientType": 5, + "emailTemplate": { + "name": "SYSTEM Notify Submitter of Advisor Approved Submission", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_APPROVE_SUBMISSION", + "recipientType": 0, + "emailTemplate": { + "name": "SYSTEM Notify Assignee of Advisor Approved Submission", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_CLEAR_APPROVE_SUBMISSION", + "recipientType": 5, + "emailTemplate": { + "name": "SYSTEM Notify Submitter of Advisor Cleared Submission Approval", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_CLEAR_APPROVE_SUBMISSION", + "recipientType": 0, + "emailTemplate": { + "name": "SYSTEM Notify Assignee of Advisor Cleared Submission Approval", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_APPROVE_EMBARGO", + "recipientType": 5, + "emailTemplate": { + "name": "SYSTEM Notify Submitter of Advisor Approved Embargo", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_APPROVE_EMBARGO", + "recipientType": 0, + "emailTemplate": { + "name": "SYSTEM Notify Assignee of Advisor Approved Embargo", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_CLEAR_APPROVE_EMBARGO", + "recipientType": 5, + "emailTemplate": { + "name": "SYSTEM Notify Submitter of Advisor Cleared Embargo Approval", + "systemRequired": true + } + }, + { + "isSystem": true, + "isDisabled": true, + "action": "ADVISOR_CLEAR_APPROVE_EMBARGO", + "recipientType": 0, + "emailTemplate": { + "name": "SYSTEM Notify Assignee of Advisor Cleared Embargo Approval", + "systemRequired": true + } + } ] } diff --git a/src/main/resources/settings/SYSTEM_Defaults.json b/src/main/resources/settings/SYSTEM_Defaults.json index 67d6c08936..0088fd85cd 100644 --- a/src/main/resources/settings/SYSTEM_Defaults.json +++ b/src/main/resources/settings/SYSTEM_Defaults.json @@ -133,9 +133,6 @@ { "left_logo": "resources/images/default-left-logo.png" }, - { - "right_logo": "resources/images/default-right-logo.gif" - }, { "custom_css": "/* Insert Custom CSS here! */" }, diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index d25abb8094..c3c06945be 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -140,7 +140,7 @@ -
+
@@ -164,7 +164,11 @@
  • Accessibility
  • - +
  • + + Vireo Logo + +
  • diff --git a/src/main/webapp/app/controllers/organizationSettingsController.js b/src/main/webapp/app/controllers/organizationSettingsController.js index 21da1f7364..e6af6f3d21 100644 --- a/src/main/webapp/app/controllers/organizationSettingsController.js +++ b/src/main/webapp/app/controllers/organizationSettingsController.js @@ -40,7 +40,8 @@ vireo.controller('OrganizationSettingsController', function ($controller, $scope $scope.getSelectedOrganizationEmailWorkflowRules = function () { if ($scope.getSelectedOrganizationId()) { - return $scope.selectedOrganization.emailWorkflowRules; + return ($scope.selectedOrganization?.emailWorkflowRules || []) + .concat($scope.selectedOrganization?.emailWorkflowRulesByAction || []); } }; diff --git a/src/main/webapp/app/controllers/settings/emailWorkflowRulesController.js b/src/main/webapp/app/controllers/settings/emailWorkflowRulesController.js index 724fa792d9..07bc50d005 100644 --- a/src/main/webapp/app/controllers/settings/emailWorkflowRulesController.js +++ b/src/main/webapp/app/controllers/settings/emailWorkflowRulesController.js @@ -4,6 +4,46 @@ vireo.controller("EmailWorkflowRulesController", function ($controller, $scope, $scope: $scope })); + $scope.active = 'email-by-status'; + + $scope.isByStatus = function () { + return $scope.active === 'email-by-status'; + }; + + $scope.isByAction = function () { + return $scope.active === 'email-by-action'; + }; + + /** + * The enum property must match one of the Action in src/main/java/org/tdl/vireo/model/Action.java + */ + $scope.submissionActions = [ + { + enum: 'STUDENT_MESSAGE', + name: 'Student Adds Message' + }, + { + enum: 'ADVISOR_MESSAGE', + name: 'Advisor Adds Message' + }, + { + enum: 'ADVISOR_APPROVE_SUBMISSION', + name: 'Advisor Approves Submission' + }, + { + enum: 'ADVISOR_CLEAR_APPROVE_SUBMISSION', + name: 'Advisor Clears Submission Approval' + }, + { + enum: 'ADVISOR_APPROVE_EMBARGO', + name: 'Advisor Approved Embargo' + }, + { + enum: 'ADVISOR_CLEAR_APPROVE_EMBARGO', + name: 'Advisor Clears Submission Embargo' + } + ]; + $scope.submissionStatuses = SubmissionStatusRepo.getAll(); $scope.emailTemplates = EmailTemplateRepo.getAll(); $scope.emailRecipientType = EmailRecipientType; @@ -31,7 +71,7 @@ vireo.controller("EmailWorkflowRulesController", function ($controller, $scope, $scope.closeModal(); }; - $scope.addEmailWorkflowRule = function (newTemplate, newRecipient, submissionStatus) { + $scope.addEmailWorkflowRule = function (newTemplate, newRecipient, statusOrAction) { var recipient = angular.copy(newRecipient); var organization = $scope.getSelectedOrganization(); organization.$dirty = true; @@ -40,10 +80,13 @@ vireo.controller("EmailWorkflowRulesController", function ($controller, $scope, recipient.data = recipient.data.id; } - OrganizationRepo.addEmailWorkflowRule(organization, newTemplate.id, recipient, submissionStatus.id).then(function () { + const emailWorkflowRuleAdded = statusOrAction?.id + ? OrganizationRepo.addEmailWorkflowRule(organization, newTemplate.id, recipient, statusOrAction?.id) + : OrganizationRepo.addEmailWorkflowRuleByAction(organization, newTemplate.id, recipient, statusOrAction?.enum) + + emailWorkflowRuleAdded.then(function () { $scope.resetEmailWorkflowRule(); }); - }; $scope.openEditEmailWorkflowRule = function (rule) { @@ -76,7 +119,11 @@ vireo.controller("EmailWorkflowRulesController", function ($controller, $scope, $scope.emailWorkflowRuleToEdit.emailRecipient.data = $scope.emailWorkflowRuleToEdit.emailRecipient.data.id; } - OrganizationRepo.editEmailWorkflowRule(organization, $scope.emailWorkflowRuleToEdit).then(function () { + const emailWorkflowRuleEdited = $scope.emailWorkflowRuleToEdit?.submissionStatus?.id + ? OrganizationRepo.editEmailWorkflowRule(organization, $scope.emailWorkflowRuleToEdit) + : OrganizationRepo.editEmailWorkflowRuleByAction(organization, $scope.emailWorkflowRuleToEdit); + + emailWorkflowRuleEdited.then(function () { $scope.resetEditEmailWorkflowRule(); }); }; @@ -95,8 +142,14 @@ vireo.controller("EmailWorkflowRulesController", function ($controller, $scope, organization.$dirty = true; $scope.emailWorkflowRuleDeleteWorking = true; - OrganizationRepo.removeEmailWorkflowRule(organization, $scope.emailWorkflowRuleToDelete).then(function () { + + const emailWorkflowRuleDeleted = $scope.emailWorkflowRuleToDelete?.submissionStatus?.id + ? OrganizationRepo.removeEmailWorkflowRule(organization, $scope.emailWorkflowRuleToDelete) + : OrganizationRepo.removeEmailWorkflowRuleByAction(organization, $scope.emailWorkflowRuleToDelete); + + emailWorkflowRuleDeleted.then(function () { $scope.emailWorkflowRuleDeleteWorking = false; + $scope.resetEditEmailWorkflowRule(); }); }; @@ -104,14 +157,18 @@ vireo.controller("EmailWorkflowRulesController", function ($controller, $scope, var organization = $scope.getSelectedOrganization(); organization.$dirty = true; - OrganizationRepo.changeEmailWorkflowRuleActivation(organization, rule).then(function () { + const ruleActivationChanged = rule?.submissionStatus + ? OrganizationRepo.changeEmailWorkflowRuleActivation(organization, rule) + : OrganizationRepo.changeEmailWorkflowRuleByActionActivation(organization, rule); + + ruleActivationChanged.then(function () { changeEmailWorkflowRuleActivation = false; }); }; $scope.cancelDeleteEmailWorkflowRule = function () { $scope.emailWorkflowRuleDeleteWorking = false; - $scope.closeModal(); + $scope.resetEditEmailWorkflowRule(); }; }); diff --git a/src/main/webapp/app/controllers/settings/lookAndFeelController.js b/src/main/webapp/app/controllers/settings/lookAndFeelController.js index 40c56db3f5..4749b3ba10 100644 --- a/src/main/webapp/app/controllers/settings/lookAndFeelController.js +++ b/src/main/webapp/app/controllers/settings/lookAndFeelController.js @@ -14,8 +14,6 @@ vireo.controller("LookAndFeelController", function ($scope, $controller, $q, Fil $scope.modalData.logoLeft = $scope.settings.configurable.lookAndFeel.left_logo.value; - $scope.modalData.logoRight = $scope.settings.configurable.lookAndFeel.right_logo.value; - $scope.previewLogo = function (files) { if (files.length > 0) { previewLogo(files[0]).then(function (result) { @@ -101,10 +99,6 @@ vireo.controller("LookAndFeelController", function ($scope, $controller, $q, Fil $scope.modalData.logoLeft = newLogoConfiguration.value; $scope.settings.configurable.lookAndFeel.left_logo = newLogoConfiguration.value; } - if (newLogoConfiguration.name == "right_logo") { - $scope.modalData.logoRight = newLogoConfiguration.value; - $scope.settings.configurable.lookAndFeel.right_logo = newLogoConfiguration.value; - } $scope.resetModalData(); } diff --git a/src/main/webapp/app/controllers/settings/whoHasAccessController.js b/src/main/webapp/app/controllers/settings/whoHasAccessController.js index 8228a361fb..971ffb4e60 100644 --- a/src/main/webapp/app/controllers/settings/whoHasAccessController.js +++ b/src/main/webapp/app/controllers/settings/whoHasAccessController.js @@ -32,7 +32,12 @@ vireo.controller('WhoHasAccessController', function ($controller, $location, $sc user.role = role; } user.dirty(true); - user.save(); + user.save().then(response => { + if (angular.fromJson(response?.body)?.meta?.status === 'SUCCESS') { + $scope.unassignableUsersTable.reload(); + $scope.assignableUsersTable.reload(); + } + }); }; $scope.setRole = function(user) { diff --git a/src/main/webapp/app/controllers/submission/adminSubmissionViewController.js b/src/main/webapp/app/controllers/submission/adminSubmissionViewController.js index a622098c57..d6401b3114 100644 --- a/src/main/webapp/app/controllers/submission/adminSubmissionViewController.js +++ b/src/main/webapp/app/controllers/submission/adminSubmissionViewController.js @@ -382,12 +382,10 @@ vireo.controller("AdminSubmissionViewController", function ($anchorScroll, $cont }; $scope.getPattern = function (doctype) { - var pattern = "*"; var fieldPredicate; - var i; - for(i in $scope.fieldPredicates) { - if($scope.fieldPredicates[i].value === doctype) { + for (var i in $scope.fieldPredicates) { + if ($scope.fieldPredicates[i].value === doctype) { fieldPredicate = $scope.fieldPredicates[i]; break; } @@ -395,17 +393,11 @@ vireo.controller("AdminSubmissionViewController", function ($anchorScroll, $cont if (fieldPredicate !== undefined) { var fieldProfile = $scope.submission.getFieldProfileByPredicate(fieldPredicate); - if (angular.isDefined(fieldProfile.controlledVocabulary)) { - var cv = fieldProfile.controlledVocabulary; - pattern = ""; - for (i in cv.dictionary) { - var word = cv.dictionary[i]; - pattern += pattern.length > 0 ? (",." + word.name) : ("." + word.name); - } - } + + return FileUploadService.getPattern(fieldProfile); } - return pattern; + return '*'; }; $scope.queueUpload = function (files) { diff --git a/src/main/webapp/app/controllers/submission/submissionListController.js b/src/main/webapp/app/controllers/submission/submissionListController.js index 8a78aa3b46..8fb15ae800 100644 --- a/src/main/webapp/app/controllers/submission/submissionListController.js +++ b/src/main/webapp/app/controllers/submission/submissionListController.js @@ -1,4 +1,4 @@ -vireo.controller("SubmissionListController", function (NgTableParams, $controller, $filter, $location, $q, $scope, ControlledVocabularyRepo, CustomActionDefinitionRepo, DepositLocationRepo, DocumentTypeRepo, EmailRecipient, EmailRecipientType, EmailTemplateRepo, EmbargoRepo, FieldPredicateRepo, FieldValueRepo, ManagerFilterColumnRepo, ManagerSubmissionListColumnRepo, NamedSearchFilterGroup, OrganizationRepo, OrganizationCategoryRepo, PackagerRepo, SavedFilter, SavedFilterRepo, SidebarService, SubmissionListColumnRepo, SubmissionRepo, SubmissionStatusRepo, UserRepo, UserSettings, WsApi) { +vireo.controller("SubmissionListController", function (NgTableParams, $controller, $filter, $location, $q, $scope, ControlledVocabularyRepo, CustomActionDefinitionRepo, DepositLocationRepo, DocumentTypeRepo, EmailRecipient, EmailRecipientType, EmailTemplateRepo, EmbargoRepo, FieldPredicateRepo, FieldValueRepo, ManagerFilterColumnRepo, ManagerSubmissionListColumnRepo, NamedSearchFilterGroup, OrganizationRepo, OrganizationCategoryRepo, PackagerRepo, SavedFilter, SavedFilterRepo, SidebarService, SubmissionListColumnRepo, SubmissionRepo, SubmissionStatusRepo, UserRepo, UserSettings, $window, WsApi) { angular.extend(this, $controller('AbstractController', { $scope: $scope @@ -1002,8 +1002,14 @@ vireo.controller("SubmissionListController", function (NgTableParams, $controlle var disabledFilterColumnOptions = createDisabledColumnOptions(); - $scope.viewSubmission = function (submission) { - $location.path("/admin/view/" + submission.id); + $scope.viewSubmission = function (event, submission) { + const url = $location.absUrl(); + if (event.ctrlKey || event.metaKey) { + $window.open(url.replace('/list', '/view/' + submission.id)); + event.stopPropagation(); + } else { + $location.path('/admin/view/' + submission.id); + }; }; SidebarService.addBoxes([{ diff --git a/src/main/webapp/app/controllers/userRepoController.js b/src/main/webapp/app/controllers/userRepoController.js index 3660baea1c..6ee9acb50b 100644 --- a/src/main/webapp/app/controllers/userRepoController.js +++ b/src/main/webapp/app/controllers/userRepoController.js @@ -48,15 +48,6 @@ vireo.controller('UserRepoController', function ($controller, $location, $scope, }] }; - $scope.table = TableFactory.buildTable({ - pageNumber: sessionStorage.getItem('users-page') ? sessionStorage.getItem('users-page') : 1, - pageSize: sessionStorage.getItem('users-size') ? sessionStorage.getItem('users-size') : 10, - filters: {}, - counts: [5, 10, 25, 50, 100], - name: 'users', - repo: $scope.userRepo - }); - $scope.roles = {}; $scope.updateRole = function(user, role) { diff --git a/src/main/webapp/app/directives/fieldProfileDirective.js b/src/main/webapp/app/directives/fieldProfileDirective.js index 42893c781b..433c86c91c 100644 --- a/src/main/webapp/app/directives/fieldProfileDirective.js +++ b/src/main/webapp/app/directives/fieldProfileDirective.js @@ -153,16 +153,7 @@ vireo.directive("field", function ($controller, $filter, $q, $timeout, Controlle }; $scope.getPattern = function () { - var pattern = "*"; - if(angular.isDefined($scope.profile.controlledVocabulary)) { - var cv = $scope.profile.controlledVocabulary; - pattern = ""; - for (var i in cv.dictionary) { - var word = cv.dictionary[i]; - pattern += pattern.length > 0 ? (",." + word.name) : ("." + word.name); - } - } - return pattern; + return FileUploadService.getPattern($scope.profile); }; $scope.queueUpload = function (files) { diff --git a/src/main/webapp/app/directives/ngTableAccessibilityDirective.js b/src/main/webapp/app/directives/ngTableAccessibilityDirective.js new file mode 100644 index 0000000000..5813095681 --- /dev/null +++ b/src/main/webapp/app/directives/ngTableAccessibilityDirective.js @@ -0,0 +1,132 @@ +angular.module('vireo').directive('ngTableAccessibility', function() { + return { + restrict: 'A', + link: function(scope, element) { + console.log('ngTableAccessibility directive initialized'); + + // Function to process table headers + function processHeaders() { + // Process main headers + var headers = element[0].querySelectorAll('thead th:not(.filter)'); + if (headers && headers.length) { + console.log('Processing ' + headers.length + ' table headers'); + headers.forEach(function(header) { + if (!header.getAttribute('role')) { + header.setAttribute('role', 'columnheader'); + header.setAttribute('scope', 'col'); + + var titleText = header.textContent.trim(); + if (titleText && !header.querySelector('.sr-only')) { + var label = document.createElement('span'); + label.className = 'sr-only'; + label.textContent = titleText + ' column'; + header.insertBefore(label, header.firstChild); + } + } + }); + } + + // Process filter headers + var filterHeaders = element[0].querySelectorAll('tr.ng-table-filters th'); + if (filterHeaders && filterHeaders.length) { + console.log('Processing ' + filterHeaders.length + ' filter headers'); + filterHeaders.forEach(function(header) { + // Get the column title from data-title-text attribute + var titleText = header.getAttribute('data-title-text'); + if (titleText && !header.querySelector('.sr-only')) { + header.setAttribute('role', 'columnheader'); + header.setAttribute('scope', 'col'); + + // Add descriptive text for screen readers + var label = document.createElement('span'); + label.className = 'sr-only'; + label.textContent = titleText + ' filter'; + + // Insert at the beginning of the header + header.insertBefore(label, header.firstChild); + } + }); + } + + // Ensure table has proper role + element[0].setAttribute('role', 'grid'); + var tbody = element[0].querySelector('tbody'); + if (tbody) { + tbody.setAttribute('role', 'rowgroup'); + } + } + + // Function to process table cells + function processCells() { + var rows = element[0].querySelectorAll('tbody tr'); + if (rows && rows.length) { + console.log('Processing ' + rows.length + ' table rows'); + rows.forEach(function(row, rowIndex) { + row.setAttribute('role', 'row'); + + var cells = row.querySelectorAll('td'); + cells.forEach(function(cell, cellIndex) { + cell.setAttribute('role', 'gridcell'); + + var titleText = cell.getAttribute('title') || + element[0].querySelector('thead th:not(.filter):nth-child(' + (cellIndex + 1) + ')')?.textContent.trim(); + + var cellContent = cell.textContent || ''; + var hasButtons = cell.querySelector('button, a'); + var hasVisibleContent = cellContent.trim() && + !['Not Submitted', 'Not Assigned', 'No Title', 'No Primary Document'] + .includes(cellContent.trim()); + + // Handle empty or placeholder content cells + if (!hasButtons && !hasVisibleContent && titleText) { + var existingLabel = cell.querySelector('.sr-only'); + if (!existingLabel) { + var label = document.createElement('span'); + label.className = 'sr-only'; + label.textContent = 'No ' + titleText.toLowerCase() + ' available'; + cell.appendChild(label); + } + } + + // Handle action buttons + if (hasButtons) { + var buttons = cell.querySelectorAll('button, a'); + buttons.forEach(function(button) { + if (!button.getAttribute('aria-label')) { + var buttonText = button.textContent.trim(); + var rowContext = cells[0]?.textContent.trim() || 'item'; + button.setAttribute('aria-label', buttonText + ' ' + rowContext); + } + }); + } + }); + }); + } + } + + // Watch for changes in the table structure + var observer = new MutationObserver(function(mutations) { + processHeaders(); + processCells(); + }); + + // Start observing the table for dynamic changes + observer.observe(element[0], { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'style'] // Watch for class/style changes that might show/hide filters + }); + + // Initial processing + processHeaders(); + processCells(); + + // Clean up observer when scope is destroyed + scope.$on('$destroy', function() { + observer.disconnect(); + }); + } + }; +}); + diff --git a/src/main/webapp/app/directives/submissionInfoDirective.js b/src/main/webapp/app/directives/submissionInfoDirective.js index fce91fec8f..7021ccc2d1 100644 --- a/src/main/webapp/app/directives/submissionInfoDirective.js +++ b/src/main/webapp/app/directives/submissionInfoDirective.js @@ -60,7 +60,7 @@ vireo.directive("submissionInfo", function (ControlledVocabularyRepo) { } if(infoForm.hasClass("form-control")) { if(infoForm.length > 1) { - infoForm[Number($event.currentTarget.id) * 2].focus(); + infoForm[Number($event.currentTarget.id)].focus(); } else { infoForm.focus(); } diff --git a/src/main/webapp/app/repo/managedConfigurationRepo.js b/src/main/webapp/app/repo/managedConfigurationRepo.js index 04e0637d77..53b032609e 100644 --- a/src/main/webapp/app/repo/managedConfigurationRepo.js +++ b/src/main/webapp/app/repo/managedConfigurationRepo.js @@ -6,12 +6,16 @@ vireo.repo("ManagedConfigurationRepo", function ManagedConfigurationRepo($q, Man var defer = $q.defer(); + var fetching = false; + // additional repo methods and variables var fetch = function (mapping) { if (mapping.all !== undefined) { + fetching = true; return WsApi.fetch(mapping.all).then(function (res) { build(unwrap(res)).then(function () { + fetching = false; defer.resolve(configurations); }); }); @@ -42,7 +46,7 @@ vireo.repo("ManagedConfigurationRepo", function ManagedConfigurationRepo($q, Man }; configurationRepo.getAll = function () { - if (this.mapping.lazy) { + if (this.mapping.lazy && !fetching) { fetch(this.mapping); } return configurationRepo.getContents(); diff --git a/src/main/webapp/app/repo/organizationRepo.js b/src/main/webapp/app/repo/organizationRepo.js index 87f1ed6267..94be77aadf 100644 --- a/src/main/webapp/app/repo/organizationRepo.js +++ b/src/main/webapp/app/repo/organizationRepo.js @@ -297,6 +297,47 @@ vireo.repo("OrganizationRepo", function OrganizationRepo($q, Organization, RestA return WsApi.fetch(apiMapping.Organization.changeEmailWorkflowRuleActivation); }; + this.addEmailWorkflowRuleByAction = function (organization, templateId, recipient, action) { + angular.extend(apiMapping.Organization.addEmailWorkflowRule, { + 'method': organization.id + "/add-email-workflow-rule-by-action", + 'data': { + templateId: templateId, + recipient: recipient, + action: action + } + }); + + return WsApi.fetch(apiMapping.Organization.addEmailWorkflowRule); + }; + + this.removeEmailWorkflowRuleByAction = function (organization, rule) { + angular.extend(apiMapping.Organization.removeEmailWorkflowRule, { + 'method': organization.id + "/remove-email-workflow-rule-by-action/" + rule.id, + }); + + return WsApi.fetch(apiMapping.Organization.removeEmailWorkflowRule); + }; + + this.editEmailWorkflowRuleByAction = function (organization, rule) { + angular.extend(apiMapping.Organization.editEmailWorkflowRule, { + 'method': organization.id + "/edit-email-workflow-rule-by-action/" + rule.id, + 'data': { + templateId: rule.emailTemplate.id, + recipient: rule.emailRecipient + } + }); + + return WsApi.fetch(apiMapping.Organization.editEmailWorkflowRule); + }; + + this.changeEmailWorkflowRuleByActionActivation = function (organization, rule) { + angular.extend(apiMapping.Organization.changeEmailWorkflowRuleActivation, { + 'method': organization.id + "/change-email-workflow-rule-by-action-activation/" + rule.id, + }); + + return WsApi.fetch(apiMapping.Organization.changeEmailWorkflowRuleActivation); + }; + return this; }); diff --git a/src/main/webapp/app/resources/images/default-right-logo.gif b/src/main/webapp/app/resources/images/default-right-logo.gif deleted file mode 100644 index 1c918238fe..0000000000 Binary files a/src/main/webapp/app/resources/images/default-right-logo.gif and /dev/null differ diff --git a/src/main/webapp/app/resources/images/footer-logo.png b/src/main/webapp/app/resources/images/footer-logo.png new file mode 100644 index 0000000000..b56ad2b09e Binary files /dev/null and b/src/main/webapp/app/resources/images/footer-logo.png differ diff --git a/src/main/webapp/app/resources/images/logo.png b/src/main/webapp/app/resources/images/logo.png index 235e34c3e6..b798f69a7f 100644 Binary files a/src/main/webapp/app/resources/images/logo.png and b/src/main/webapp/app/resources/images/logo.png differ diff --git a/src/main/webapp/app/resources/styles/sass/app.scss b/src/main/webapp/app/resources/styles/sass/app.scss index bf31d3965a..1ba8ab43ed 100644 --- a/src/main/webapp/app/resources/styles/sass/app.scss +++ b/src/main/webapp/app/resources/styles/sass/app.scss @@ -30,6 +30,10 @@ main footer { margin-bottom: 65px; } +.mb-1 { + margin-bottom: 10px; +} + .ml-1 { margin-left: 10px; } diff --git a/src/main/webapp/app/services/fileUploadService.js b/src/main/webapp/app/services/fileUploadService.js index 1f5e0f1d2b..b58fd9abd4 100644 --- a/src/main/webapp/app/services/fileUploadService.js +++ b/src/main/webapp/app/services/fileUploadService.js @@ -2,6 +2,25 @@ vireo.service("FileUploadService", function ($q, FieldValue, FileService) { var FileUploadService = this; + FileUploadService.getPattern = function (fieldProfile) { + var pattern = '*'; + + if (fieldProfile?.fieldPredicate?.value === '_doctype_primary') { + return '.pdf' + } + + if (angular.isDefined(fieldProfile?.controlledVocabulary)) { + var cv = fieldProfile?.controlledVocabulary; + pattern = ''; + for (var i in cv.dictionary) { + var word = cv.dictionary[i]; + pattern += pattern.length > 0 ? (",." + word.name) : ("." + word.name); + } + } + + return pattern; + }; + FileUploadService.uploadFile = function (submission, fieldValue) { return FileService.upload({ 'endpoint': '', diff --git a/src/main/webapp/app/views/admin/list.html b/src/main/webapp/app/views/admin/list.html index fcbdd573f3..d2d958b97d 100644 --- a/src/main/webapp/app/views/admin/list.html +++ b/src/main/webapp/app/views/admin/list.html @@ -31,7 +31,7 @@

    List ETDs

    - + {{displaySubmissionProperty(row, col)}} diff --git a/src/main/webapp/app/views/admin/settings/application/lookAndFeel.html b/src/main/webapp/app/views/admin/settings/application/lookAndFeel.html index eb930b0ee8..c1be2e06da 100644 --- a/src/main/webapp/app/views/admin/settings/application/lookAndFeel.html +++ b/src/main/webapp/app/views/admin/settings/application/lookAndFeel.html @@ -522,11 +522,11 @@ - +
    -
    Logos - +
    Logo +
    @@ -534,35 +534,19 @@

    - Left Logo + Header Logo

    - +
    - -
    -
    -

    - Right Logo - - -

    - -
    - -
    - -
    - +
    diff --git a/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRule.html b/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRule.html index cf10b7fe47..8dadb43446 100644 --- a/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRule.html +++ b/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRule.html @@ -1,4 +1,4 @@ - +
    @@ -16,7 +16,10 @@ - + @@ -46,7 +49,7 @@ diff --git a/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRules.html b/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRules.html index 6c25ffaf59..c79dd59e77 100644 --- a/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRules.html +++ b/src/main/webapp/app/views/admin/settings/organization/emailWorkflowRules.html @@ -4,9 +4,35 @@

    Manage {{getSelectedOrganizationName()}} Email Workflow Rules

    - - {{status.name}} - + + + +
    +
    +
    + + {{ status.name }} + +
    +
    +
    + +
    +
    +
    + + {{ action.name }} + +
    +
    +
    -

    Add new File Type

    +

    Add new File Type

    diff --git a/src/main/webapp/app/views/directives/dropZone.html b/src/main/webapp/app/views/directives/dropZone.html index 8b1d8b8cb7..b708002fc0 100644 --- a/src/main/webapp/app/views/directives/dropZone.html +++ b/src/main/webapp/app/views/directives/dropZone.html @@ -1,8 +1,8 @@
    -
    @@ -10,3 +10,4 @@

    Not an accepted file type!

    + diff --git a/src/main/webapp/app/views/directives/fieldProfile.html b/src/main/webapp/app/views/directives/fieldProfile.html index b4c3fd7a86..e77adc5305 100644 --- a/src/main/webapp/app/views/directives/fieldProfile.html +++ b/src/main/webapp/app/views/directives/fieldProfile.html @@ -1,7 +1,7 @@
    -
    @@ -36,9 +37,11 @@ typeahead-no-results="noResults" typeahead-on-select="saveWithCV(fieldValue, $item)" typeahead-loading="(!fieldValue.fieldPredicate.id || !typeAheads[fieldValue.fieldPredicate.id]) ? angular.noop : typeAheads[fieldValue.fieldPredicate.id].loading" + aria-label="{{profile.gloss}} contact info" /> - - + +
    + diff --git a/src/main/webapp/app/views/inputtype/input-contact_select.html b/src/main/webapp/app/views/inputtype/input-contact_select.html index 04c3189633..b857a3a7c6 100644 --- a/src/main/webapp/app/views/inputtype/input-contact_select.html +++ b/src/main/webapp/app/views/inputtype/input-contact_select.html @@ -1,24 +1,25 @@ -
    - - -
    @@ -27,9 +28,11 @@ class="form-control" ng-model="field.contacts" placeholder="{{fieldValue.contacts[0]}}" + aria-label="Contact information" disabled> - - + + + diff --git a/src/main/webapp/app/views/inputtype/input-date.html b/src/main/webapp/app/views/inputtype/input-date.html index 95bdf2666a..a961220a4a 100644 --- a/src/main/webapp/app/views/inputtype/input-date.html +++ b/src/main/webapp/app/views/inputtype/input-date.html @@ -5,6 +5,8 @@ + diff --git a/src/main/webapp/app/views/inputtype/input-degreedate.html b/src/main/webapp/app/views/inputtype/input-degreedate.html index 1212f98447..b7491ee9ec 100644 --- a/src/main/webapp/app/views/inputtype/input-degreedate.html +++ b/src/main/webapp/app/views/inputtype/input-degreedate.html @@ -6,6 +6,8 @@ type="text" class="form-control" uib-datepicker-popup="MMMM yyyy" + id="field-{{profile.fieldPredicate.value}}" + name="{{profile.fieldPredicate.value}}" datepicker-options="datepickerOptions" ng-model="fieldValue.valuePopup" ng-click="date=true" diff --git a/src/main/webapp/app/views/inputtype/input-email.html b/src/main/webapp/app/views/inputtype/input-email.html index c7fbc0814b..61fdc3095c 100644 --- a/src/main/webapp/app/views/inputtype/input-email.html +++ b/src/main/webapp/app/views/inputtype/input-email.html @@ -1,7 +1,8 @@
    + diff --git a/src/main/webapp/app/views/inputtype/input-radio.html b/src/main/webapp/app/views/inputtype/input-radio.html index 1175cb6be9..7d12938ce4 100644 --- a/src/main/webapp/app/views/inputtype/input-radio.html +++ b/src/main/webapp/app/views/inputtype/input-radio.html @@ -1,21 +1,22 @@
    - +
    -
    +
    {{word.name}}
    {{word.definition}}
    -
    + diff --git a/src/main/webapp/app/views/inputtype/input-select.html b/src/main/webapp/app/views/inputtype/input-select.html index 0922c3ded1..34899279e6 100644 --- a/src/main/webapp/app/views/inputtype/input-select.html +++ b/src/main/webapp/app/views/inputtype/input-select.html @@ -1,23 +1,26 @@
    - - -
    + + diff --git a/src/main/webapp/app/views/inputtype/input-tel.html b/src/main/webapp/app/views/inputtype/input-tel.html index 135090c665..39ce7953e4 100644 --- a/src/main/webapp/app/views/inputtype/input-tel.html +++ b/src/main/webapp/app/views/inputtype/input-tel.html @@ -1,7 +1,8 @@
    -