diff --git a/build.gradle.kts b/build.gradle.kts index 5648962..73117eb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("org.orph2020.pst.common-plugin") } -version = "0.7" +version = "1.0" dependencies { implementation("io.quarkus:quarkus-mailer") @@ -17,6 +17,7 @@ dependencies { implementation("io.quarkus:quarkus-hibernate-orm") implementation("io.quarkus:quarkus-rest-client-reactive-jackson") implementation("io.quarkus:quarkus-hibernate-validator") + implementation("io.quarkus:quarkus-liquibase") implementation("io.quarkus:quarkus-jdbc-postgresql") implementation("io.quarkus:quarkus-kubernetes-service-binding") @@ -32,6 +33,9 @@ dependencies { implementation("uk.ac.starlink:stil:4.1.4") implementation("commons-io:commons-io:2.15.1") + + implementation("org.apache.poi:poi:5.2.5") + implementation("org.apache.poi:poi-ooxml:5.2.5") } diff --git a/src/main/java/org/orph2020/pst/apiimpl/rest/JustificationsResource.java b/src/main/java/org/orph2020/pst/apiimpl/rest/JustificationsResource.java index dca8b3c..6baf9fd 100644 --- a/src/main/java/org/orph2020/pst/apiimpl/rest/JustificationsResource.java +++ b/src/main/java/org/orph2020/pst/apiimpl/rest/JustificationsResource.java @@ -207,7 +207,7 @@ public Response downloadReviewerZip(@PathParam("proposalCode") Long proposalCode SubmittedProposal proposal = findObject(SubmittedProposal.class, proposalCode); - return Response.ok(proposalResource.CreateZipFile("Review.zip", proposal, true )) + return Response.ok(proposalResource.CreateZipFile("Review.zip", proposal, true, false )) .header("Content-Disposition", "attachment; filename=" + "Review.zip") .build(); diff --git a/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalCyclesResource.java b/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalCyclesResource.java index 4404f1f..8827ca6 100644 --- a/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalCyclesResource.java +++ b/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalCyclesResource.java @@ -8,6 +8,10 @@ import jakarta.persistence.Query; import jakarta.transaction.Transactional; import jakarta.ws.rs.core.Response; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.eclipse.microprofile.jwt.JsonWebToken; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @@ -22,6 +26,8 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.*; import java.util.concurrent.atomic.AtomicReference; @@ -36,6 +42,8 @@ public class ProposalCyclesResource extends ObjectResourceBase { SubjectMapResource subjectMapResource; @Inject JsonWebToken userInfo; + @Inject + ProposalDocumentStore proposalDocumentStore; private static final String notOnTACmsg = "This endpoint is restricted to TAC members only"; @@ -523,4 +531,70 @@ public Response replaceCycleObservatory( return result; } + @GET + @Path("{cycleCode}/excelReviews") + @Operation(summary="Create and download an excel sheet of all submitted proposals and their review scores") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response ExcelReviews(@PathParam("cycleCode") Long cycleCode) { + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + // Get the whole proposal cycle + ProposalCycle proposalCycle = findObject(ProposalCycle.class, cycleCode); + + // Create a sheet + XSSFSheet sheet = workbook.createSheet(proposalCycle.getCode()); + + // count the reviewers for the number of columns + HashMap allReviewers = new HashMap<>(); + Integer reviewerColumnPos = 0; + for(SubmittedProposal submittedProposal : proposalCycle.getSubmittedProposals()) + for(ProposalReview review : submittedProposal.getReviews()) + allReviewers.putIfAbsent(review.getReviewer(), reviewerColumnPos++); + + // write headers + int rowNum = 0; + + Row row = sheet.createRow(rowNum++); + Cell cell = row.createCell(0); + cell.setCellValue("Code"); + Cell cellTitle = row.createCell(1); + cellTitle.setCellValue("Title"); + + for(Reviewer reviewer : allReviewers.keySet()) { + Cell rCell = row.createCell(2 + allReviewers.get(reviewer)); + rCell.setCellValue(reviewer.getPerson().getFullName()); + } + + // write data + for(SubmittedProposal submittedProposal: proposalCycle.getSubmittedProposals()) { + int cellNum = 0; + Row submittedRow = sheet.createRow(rowNum++); + Cell code = submittedRow.createCell(cellNum++); + code.setCellValue(submittedProposal.getProposalCode()); + Cell title = submittedRow.createCell(cellNum++); + title.setCellValue(submittedProposal.getTitle()); + //Populate review scores + for(ProposalReview review : submittedProposal.getReviews()) { + Cell reviewScore = submittedRow.createCell(cellNum + allReviewers.get(review.getReviewer())); + reviewScore.setCellValue(review.getScore()); + } + + } + + String filename = "/Reviews for " + proposalCycle.getCode() + ".xlsx"; + try (FileOutputStream out = new FileOutputStream(proposalDocumentStore.getStoreRoot() + filename)) { + workbook.write(out); + } catch (IOException e) { + // error writing excel workbook to file + return Response.status(500).build(); + } + + return Response.ok(proposalDocumentStore.fetchFile(filename)) + .header("Content-Disposition", "attachment; filename=" + filename) + .build(); + } + catch (Exception e) { + return Response.status(500).build(); + } + } + } diff --git a/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalResource.java b/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalResource.java index 36d40d3..e5b521f 100644 --- a/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalResource.java +++ b/src/main/java/org/orph2020/pst/apiimpl/rest/ProposalResource.java @@ -955,24 +955,28 @@ private String observationsTable(List observations) { } - public File CreateZipFile(String zipFileName, AbstractProposal proposal, boolean anonymise) throws IOException { + public File CreateZipFile(String zipFileName, AbstractProposal proposal, boolean anonymise, boolean genericExportFilenames) throws IOException { // Create zip file File myZipFile = new File(zipFileName); ZipOutputStream zipOs = new ZipOutputStream(new FileOutputStream(myZipFile)); - String jsonFilename = "proposal.json"; + String projFilename = "proposal"; // Write proposal data (if given) if(proposal != null) { + if (proposal instanceof SubmittedProposal) { + projFilename = ((SubmittedProposal) proposal).getProposalCode() + "." + + proposal.getTitle().replaceAll("[\\\\/:*?\"<>|]", "_") + .substring(0, Math.min(proposal.getTitle().length(), 30)); + } + if (proposal instanceof ObservingProposal) { + projFilename = proposal.getTitle().replaceAll("[\\\\/:*?\"<>|]", "_") + .substring(0, Math.min(proposal.getTitle().length(), 30)); + } + if(!anonymise) { //json of Proposal ByteArrayInputStream bais = new ByteArrayInputStream(writeAsJsonString(proposal).getBytes()); - if (proposal instanceof SubmittedProposal) { - jsonFilename = ((SubmittedProposal) proposal).getProposalCode() - + proposal.getTitle().substring(0, Math.min(proposal.getTitle().length(), 31)) - + ".json"; - } - - zipOs.putNextEntry(new ZipEntry(jsonFilename)); + zipOs.putNextEntry(new ZipEntry(genericExportFilenames?"propsal.json":projFilename+ ".json")); byte[] bytes = new byte[1024]; int length; @@ -987,19 +991,25 @@ public File CreateZipFile(String zipFileName, AbstractProposal proposal, boolean // HTML overview page overviewHTMLDocument(proposal, anonymise); - zipOs.putNextEntry(new ZipEntry("Overview.html")); + zipOs.putNextEntry(new ZipEntry(genericExportFilenames?"Overview.html":projFilename + ".html")); Files.copy(proposalDocumentStore.fetchFile(proposal.getId() + "/Overview.html").toPath(), zipOs); zipOs.flush(); zipOs.closeEntry(); // Add all supporting documents unless anonymised, then only add compiled justification for(SupportingDocument doc: proposal.getSupportingDocuments()) { + // If anonymise is true, only include the compiled justifications pdf if(!anonymise || doc.getTitle().equals(justificationsResource.jobName+".pdf")) { - zipOs.putNextEntry(new ZipEntry(doc.getTitle())); - Files.copy(proposalDocumentStore - .fetchFile(proposalDocumentStore.getSupportingDocumentsPath(proposal.getId()) - + doc.getTitle()).toPath(), - zipOs); + // If genericExportFilenames is false, rename compiled justifications pdf + if(!genericExportFilenames && doc.getTitle().equals(justificationsResource.jobName+".pdf")) + zipOs.putNextEntry(new ZipEntry(projFilename + ".pdf")); + else + zipOs.putNextEntry(new ZipEntry(doc.getTitle())); + + Files.copy(proposalDocumentStore.fetchFile( + proposalDocumentStore.getSupportingDocumentsPath(proposal.getId()) + + doc.getTitle()).toPath(), + zipOs); zipOs.flush(); zipOs.closeEntry(); } @@ -1020,11 +1030,12 @@ public Response exportProposalZip(@PathParam("proposalCode")Long proposalCode) throws WebApplicationException, IOException { ObservingProposal proposalForExport = singleObservingProposal(proposalCode); String filename = "Export." - + proposalForExport.getTitle().substring(0, Math.min(proposalForExport.getTitle().length(), 31)) + + proposalForExport.getTitle().replaceAll("[\\\\/:*?\"<>|]", "_") + .substring(0, Math.min(proposalForExport.getTitle().length(), 30)) + ".zip"; File myZipFile = CreateZipFile(proposalDocumentStore.getStoreRoot() + proposalCode.toString() - + "/" + filename, proposalForExport, false); + + "/" + filename, proposalForExport, false, true); return Response.ok(myZipFile) .header("Content-Disposition", "attachment; filename=" + filename) diff --git a/src/main/java/org/orph2020/pst/apiimpl/rest/SubmittedProposalResource.java b/src/main/java/org/orph2020/pst/apiimpl/rest/SubmittedProposalResource.java index 340102e..0c6b1bf 100644 --- a/src/main/java/org/orph2020/pst/apiimpl/rest/SubmittedProposalResource.java +++ b/src/main/java/org/orph2020/pst/apiimpl/rest/SubmittedProposalResource.java @@ -364,15 +364,16 @@ public Response downloadAdminZip(@PathParam("submittedProposalId") Long submitte SubmittedProposal proposal = findObject(SubmittedProposal.class, submittedProposalId); - String filename = proposal.getProposalCode() - + proposal.getTitle().substring(0, Math.min(proposal.getTitle().length(), 31)) + String filename = proposal.getProposalCode() + "." + + proposal.getTitle().replaceAll("[\\\\/:*?\"<>|]", "_") + .substring(0, Math.min(proposal.getTitle().length(), 30)) + ".zip"; // Generate the Admin's pdf view of this submitted proposal justificationsResource.createTACAdminPDF(submittedProposalId); File myZipFile = proposalResource.CreateZipFile(proposalDocumentStore.getStoreRoot() - + submittedProposalId + "/" + filename, proposal, false); + + submittedProposalId + "/" + filename, proposal, false, false); return Response.ok(myZipFile) .header("Content-Disposition", "attachment; filename=" + "Example.zip") diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index baa7145..2fa1b3c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,13 +17,17 @@ quarkus.mailer.start-tls=DISABLED #https://quarkus.io/guides/hibernate-orm#multiple-persistence-units #quarkus.hibernate-orm.datasource=pstdb quarkus.hibernate-orm.packages=org.ivoa.dm,org.ivoa.vodml.stdtypes,org.orph2020.pst.apiimpl.entities -%dev.quarkus.hibernate-orm.database.generation=drop-and-create +%dev,test.quarkus.hibernate-orm.database.generation=drop-and-create #do not let the database update in production - has to be done manually now. %prod.quarkus.hibernate-orm.database.generation=none #note latest documentation has this %prod.quarkus.hibernate-orm.schema-management.strategy = none quarkus.hibernate-orm.database.generation.create-schemas=true quarkus.hibernate-orm.database.generation.halt-on-error=false +# Liquibase minimal config properties +%dev.quarkus.liquibase.migrate-at-start=false +%prod.quarkus.liquibase.migrate-at-start=true + quarkus.hibernate-orm.quote-identifiers.strategy = all %test.quarkus.hibernate-orm.log.sql=true