diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/components/budget/BudgetViewController.java b/src/main/java/edu/ucdavis/dss/ipa/api/components/budget/BudgetViewController.java index 273654df..cfd67ee3 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/api/components/budget/BudgetViewController.java +++ b/src/main/java/edu/ucdavis/dss/ipa/api/components/budget/BudgetViewController.java @@ -12,6 +12,7 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.servlet.View; @@ -47,6 +48,7 @@ public class BudgetViewController { @Inject InstructorTypeService instructorTypeService; @Inject ExpenseItemService expenseItemService; @Inject ExpenseItemTypeService expenseItemTypeService; + @Inject WorkloadSnapshotService workloadSnapshotService; @Value("${IPA_URL_API}") String ipaUrlApi; @@ -122,6 +124,7 @@ public BudgetScenarioView createBudgetScenario(@PathVariable long budgetId, @RequestMapping(value = "/api/budgetView/budgets/{budgetId}/budgetScenarios/{budgetScenarioId}/budgetRequest", method = RequestMethod.POST, produces = "application/json") @ResponseBody + @Transactional public BudgetScenarioView createBudgetRequestScenario(@PathVariable long budgetId, @PathVariable long budgetScenarioId, HttpServletResponse httpResponse) { @@ -140,6 +143,8 @@ public BudgetScenarioView createBudgetRequestScenario(@PathVariable long budgetI BudgetScenario budgetRequestScenario = budgetScenarioService.createBudgetRequestScenario(workGroupId, budgetScenarioId); + WorkloadSnapshot workloadSnapshot = workloadSnapshotService.create(workGroupId, budgetRequestScenario.getId()); + return budgetViewFactory.createBudgetScenarioView(budgetRequestScenario); }; @@ -181,6 +186,8 @@ public Long deleteBudgetScenario(@PathVariable long budgetScenarioId, Long workGroupId = budgetScenario.getBudget().getSchedule().getWorkgroup().getId(); authorizer.hasWorkgroupRoles(workGroupId, "academicPlanner", "reviewer"); + workloadSnapshotService.deleteByBudgetScenarioId(budgetScenarioId); + budgetScenarioService.deleteById(budgetScenarioId); return budgetScenarioId; diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/WorkloadSummaryReportController.java b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/WorkloadSummaryReportController.java index 700cfef9..04c75f35 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/WorkloadSummaryReportController.java +++ b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/WorkloadSummaryReportController.java @@ -1,20 +1,26 @@ package edu.ucdavis.dss.ipa.api.components.workloadSummaryReport; import com.amazonaws.services.s3.model.ObjectMetadata; -import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.WorkloadSummaryReportView; import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.factories.WorkloadSummaryReportViewFactory; import edu.ucdavis.dss.ipa.entities.User; import edu.ucdavis.dss.ipa.entities.UserRole; +import edu.ucdavis.dss.ipa.entities.Workgroup; +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; import edu.ucdavis.dss.ipa.security.Authorization; import edu.ucdavis.dss.ipa.security.Authorizer; import edu.ucdavis.dss.ipa.security.UrlEncryptor; import edu.ucdavis.dss.ipa.services.UserService; +import edu.ucdavis.dss.ipa.services.WorkloadAssignmentService; +import edu.ucdavis.dss.ipa.services.WorkloadSnapshotService; import edu.ucdavis.dss.ipa.utilities.EmailService; import edu.ucdavis.dss.ipa.utilities.S3Service; import java.text.ParseException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import javax.inject.Inject; @@ -29,6 +35,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @@ -39,6 +46,11 @@ public class WorkloadSummaryReportController { @Inject WorkloadSummaryReportViewFactory workloadSummaryReportViewFactory; + @Inject + WorkloadAssignmentService workloadAssignmentService; + @Inject + WorkloadSnapshotService workloadSnapshotService; + @Inject S3Service s3Service; @Inject @@ -58,11 +70,35 @@ public class WorkloadSummaryReportController { @RequestMapping(value = "/api/workloadSummaryReport/{workgroupId}/years/{year}", method = RequestMethod.GET, produces = "application/json") @ResponseBody - public WorkloadSummaryReportView getWorkloadSummaryReportView( + public List getWorkloadSummaryReportView( @PathVariable long workgroupId, @PathVariable long year) { authorizer.hasWorkgroupRoles(workgroupId, "academicPlanner", "reviewer"); - return workloadSummaryReportViewFactory.createWorkloadSummaryReportView(workgroupId, year); + return workloadAssignmentService.generateWorkloadAssignments(workgroupId, year); + } + + @RequestMapping(value = "/api/years/{year}/workloadSnapshots", method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public Map> getUserWorkgroupsSnapshots(@PathVariable long year, + HttpServletResponse httpResponse) { + User currentUser = userService.getOneByLoginId(authorization.getLoginId()); + List userWorkgroups = currentUser.getWorkgroups(); + Map> departmentSnapshots = new HashMap<>(); + + for (Workgroup userWorkgroup : userWorkgroups) { + List workloadSnapshots = new ArrayList<>(); + workloadSnapshots.addAll(workloadSnapshotService.findByWorkgroupIdAndYear(userWorkgroup.getId(), year - 1)); + workloadSnapshots.addAll(workloadSnapshotService.findByWorkgroupIdAndYear(userWorkgroup.getId(), year)); + + Map department = new HashMap<>(); + department.put("name", userWorkgroup.getName()); + department.put("workgroupId", userWorkgroup.getId()); + department.put("snapshots", workloadSnapshots); + + departmentSnapshots.put(userWorkgroup.getName(), department); + } + + return departmentSnapshots; } @RequestMapping(value = "/api/workloadSummaryReport/{workgroupIds}/years/{year}/generateExcel", method = RequestMethod.GET) @@ -114,11 +150,13 @@ public View downloadExcel(@PathVariable long[] workgroupId, @PathVariable long y } } - @RequestMapping(value = "/api/workloadSummaryReport/{workgroupId}/years/{year}/generateMultiple", method = RequestMethod.GET) + @RequestMapping(value = "/api/workloadSummaryReport/{workgroupId}/years/{year}/generateMultiple", method = RequestMethod.POST) public ResponseEntity generateMultipleDepartments(@PathVariable long workgroupId, - @PathVariable long year) { + @PathVariable long year, + @RequestBody Optional>> departmentSnapshots + ) { authorizer.isDeansOffice(); - final String fileName = year + "_Workload_Summary_Report.xlsx"; + final String fileName = year + (departmentSnapshots.isPresent() ? "_Workload_Snapshots" : "_Workload_Summary_Report") + ".xlsx"; // overwrite with empty file to update modified time s3Service.upload(fileName, new byte[0]); @@ -127,14 +165,17 @@ public ResponseEntity generateMultipleDepartments(@PathVariable long workgroupId authorization.getUserRoles().stream().filter(ur -> ur.getRole().getName().equals("academicPlanner")).map( UserRole::getWorkgroupIdentification).mapToLong(Long::longValue).toArray(); - User user = userService.getOneByLoginId(authorization.getRealUserLoginId()); String downloadUrl = ipaUrlFrontend + "/summary/" + workgroupId + "/" + year + "?mode=download"; CompletableFuture.supplyAsync( () -> { try { - return workloadSummaryReportViewFactory.createWorkloadSummaryReportBytes(workgroupIds, year); + if (departmentSnapshots.isPresent()) { + return workloadSummaryReportViewFactory.createWorkloadSummaryReportBytes(departmentSnapshots.get(), year); + } else { + return workloadSummaryReportViewFactory.createWorkloadSummaryReportBytes(workgroupIds, year); + } } catch (Exception e) { e.printStackTrace(); return null; @@ -165,11 +206,18 @@ public ResponseEntity generateMultipleDepartments(@PathVariable long workgroupId return new ResponseEntity<>(HttpStatus.ACCEPTED); } - @RequestMapping(value = "/api/workloadSummaryReport/{workgroupId}/years/{year}/downloadMultiple", method = RequestMethod.POST) - public ResponseEntity downloadMultipleDepartments(@PathVariable long workgroupId, @PathVariable long year) { + @RequestMapping(value = "/api/workloadSummaryReport/{workgroupId}/years/{year}/downloadMultiple/{file}", method = RequestMethod.POST) + public ResponseEntity downloadMultipleDepartments(@PathVariable long workgroupId, @PathVariable long year, @PathVariable String file) { authorizer.isDeansOffice(); - byte[] bytes = s3Service.download(year + "_Workload_Summary_Report.xlsx"); + String filename = null; + if (file.equals("workloadSummaries")) { + filename = year + "_Workload_Summary_Report.xlsx"; + } else if (file.equals("workloadSnapshots")) { + filename = year + "_Workload_Snapshots.xlsx"; + } + + byte[] bytes = s3Service.download(filename); ByteArrayResource resource = new ByteArrayResource(bytes); return ResponseEntity.ok() @@ -227,17 +275,30 @@ public View downloadHistoricalExcel(@PathVariable long workgroupId, @PathVariabl } @RequestMapping(value = "/api/workloadSummaryReport/years/{year}/download/status", method = RequestMethod.GET, produces = "application/json") - public Map getDownloadStatus(@PathVariable long year) { + public Map> getDownloadStatus(@PathVariable long year) { authorizer.isDeansOffice(); - ObjectMetadata metadata = s3Service.getMetadata(year + "_Workload_Summary_Report.xlsx"); - if (metadata != null) { - Map md = new HashMap<>(); - md.put("lastModified", metadata.getLastModified().getTime()); - md.put("contentLength", metadata.getContentLength()); + Map> status = new HashMap<>(); + + ObjectMetadata workloadSummaries = s3Service.getMetadata(year + "_Workload_Summary_Report.xlsx"); + ObjectMetadata workloadSnapshots = s3Service.getMetadata(year + "_Workload_Snapshots.xlsx"); + if (workloadSummaries != null) { + Map md = new HashMap<>(); + + md.put("lastModified", workloadSummaries.getLastModified().getTime()); + md.put("contentLength", workloadSummaries.getContentLength()); - return md; + status.put("workloadSummaries", md); } - return null; + + if (workloadSnapshots != null) { + Map md = new HashMap<>(); + md.put("lastModified", workloadSnapshots.getLastModified().getTime()); + md.put("contentLength", workloadSnapshots.getContentLength()); + + status.put("workloadSnapshots", md); + } + + return status.isEmpty() ? null : status; } } diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/InstructorAssignment.java b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/InstructorAssignment.java deleted file mode 100644 index 20f43b15..00000000 --- a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/InstructorAssignment.java +++ /dev/null @@ -1,80 +0,0 @@ -package edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views; - -import java.util.Arrays; -import java.util.List; - -/** - * Represents an assignment data row - */ -public class InstructorAssignment { - String department, instructorType, name, term, courseType, description, offering, lastOfferedCensus, units, - instructorNote; - Long census, previousYearCensus; - Integer plannedSeats; - Float studentCreditHours; - long year; - - public InstructorAssignment(long year, String department, String instructorType, String name, String term, - String courseType, - String description, String offering, Long census, Integer plannedSeats, - Long previousYearCensus, - String lastOfferedCensus, String units, - Float studentCreditHours, String instructorNote) { - this.year = year; - this.department = department; - this.instructorType = instructorType; - this.name = name; - this.term = term; - this.courseType = courseType; - this.description = description; - this.offering = offering; - this.census = census; - this.plannedSeats = plannedSeats; - this.previousYearCensus = previousYearCensus; - this.lastOfferedCensus = lastOfferedCensus; - this.units = units; - this.studentCreditHours = studentCreditHours; - this.instructorNote = instructorNote; - } - - public InstructorAssignment(long year, String department, String instructorType, String name, String instructorNote) { - this.year = year; - this.department = department; - this.instructorType = instructorType; - this.name = name; - this.instructorNote = instructorNote; - } - - public InstructorAssignment(long year, String department, String instructorType, String name, String term, - String courseType, - String description, String offering) { - this.year = year; - this.department = department; - this.instructorType = instructorType; - this.name = name; - this.term = term; - this.courseType = courseType; - this.description = description; - this.offering = offering; - } - - public List toList() { - return Arrays.asList( - this.year + "-" + String.valueOf(this.year + 1).substring(2, 4), - this.department, - this.instructorType.toUpperCase(), - this.name, - this.term, - this.courseType, - this.description, - this.offering, - this.census, - this.plannedSeats, - this.previousYearCensus, - this.lastOfferedCensus, - this.units, - this.studentCreditHours, - this.instructorNote - ); - } -} diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadHistoricalReportExcelView.java b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadHistoricalReportExcelView.java index db1f07a8..32ed5f2a 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadHistoricalReportExcelView.java +++ b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadHistoricalReportExcelView.java @@ -1,5 +1,6 @@ package edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views; +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; import edu.ucdavis.dss.ipa.utilities.ExcelHelper; import java.util.Arrays; import java.util.Comparator; @@ -13,16 +14,16 @@ import org.springframework.web.servlet.view.document.AbstractXlsxView; public class WorkloadHistoricalReportExcelView extends AbstractXlsxView { - private Map> instructorAssignmentsMap; + private Map> workloadAssignmentMap; - public WorkloadHistoricalReportExcelView(Map> instructorAssignmentsMap) { - this.instructorAssignmentsMap = instructorAssignmentsMap; + public WorkloadHistoricalReportExcelView(Map> workloadAssignmentMap) { + this.workloadAssignmentMap = workloadAssignmentMap; } @Override protected void buildExcelDocument(Map model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) { - List years = this.instructorAssignmentsMap.keySet().stream().sorted(Comparator.reverseOrder()).collect( + List years = this.workloadAssignmentMap.keySet().stream().sorted(Comparator.reverseOrder()).collect( Collectors.toList()); String filename = "attachment; filename=\"" + years.get(0) + " Workload Historical Report.xlsx"; @@ -37,8 +38,8 @@ protected void buildExcelDocument(Map model, Workbook workbook, "Offering", "Enrollment", "Planned Seats", "Previous Enrollment (YoY)", "Previous Enrollment (Last Offered)", "Units", "SCH", "Note")); - for (InstructorAssignment instructorAssignment : instructorAssignmentsMap.get(year)) { - ExcelHelper.writeRowToSheet(worksheet, instructorAssignment.toList()); + for (WorkloadAssignment workloadAssignment : workloadAssignmentMap.get(year)) { + ExcelHelper.writeRowToSheet(worksheet, workloadAssignment.toList()); } } diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadSummaryReportExcelView.java b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadSummaryReportExcelView.java index beb395d8..edf3ecf3 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadSummaryReportExcelView.java +++ b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/WorkloadSummaryReportExcelView.java @@ -1,46 +1,370 @@ package edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views; +import edu.ucdavis.dss.ipa.entities.Term; +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; +import edu.ucdavis.dss.ipa.entities.enums.InstructorType; import edu.ucdavis.dss.ipa.utilities.ExcelHelper; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.springframework.web.servlet.view.document.AbstractXlsxView; public class WorkloadSummaryReportExcelView extends AbstractXlsxView { - private long year; - private List instructorAssignmentList; + private final long year; + private final List workloadAssignments; + private final String snapshotName; - public WorkloadSummaryReportExcelView(List instructorAssignmentList, long year) { - this.instructorAssignmentList = instructorAssignmentList; + public WorkloadSummaryReportExcelView(List workloadAssignments, long year) { + this.workloadAssignments = workloadAssignments; this.year = year; + this.snapshotName = null; } - public static void buildRawAssignmentsSheet(Workbook wb, List instructorAssignmentList) { + public WorkloadSummaryReportExcelView(List workloadAssignments, long year, + String snapshotName) { + this.workloadAssignments = workloadAssignments; + this.year = year; + this.snapshotName = snapshotName; + } + + public static void buildRawAssignmentsSheet(Workbook wb, List workloadAssignments) { Sheet worksheet = wb.createSheet("Raw Assignments Data"); ExcelHelper.setSheetHeader(worksheet, - Arrays.asList("Year", "Department", "Instructor Type", "Name", "Term", "Course Type", "Description", + Arrays.asList("Year", "Snapshot", "Department", "Instructor Type", "Name", "Term", "Course Type", "Description", "Offering", "Enrollment", "Planned Seats", "Previous Enrollment (YoY)", "Previous Enrollment (Last Offered)", "Units", "SCH", "Note")); - for (InstructorAssignment instructorAssignment : instructorAssignmentList) { - ExcelHelper.writeRowToSheet(worksheet, instructorAssignment.toList()); + for (WorkloadAssignment workloadAssignment : workloadAssignments) { + ExcelHelper.writeRowToSheet(worksheet, workloadAssignment.toList()); } } + public void buildReportSheet(Workbook wb, List workloadAssignments) { + Sheet worksheet = wb.createSheet("Workload Summary Report"); + + List instructorSectionHeaders = + Arrays.asList("Instructor", "Term", "Description", "Offering", "Enrollment / Seats", + "Previous Enrollments (YoY)", + "Previous Enrollment (Last Offered)", "Units", "SCH", "Note"); + + Map> assignmentsByInstructorType = + generateInstructorTypeAssignmentsMap(workloadAssignments); + + Map assignedTotals = buildTotalsMap(); + Map unassignedTotals = buildTotalsMap(); + Map placeholderTotals = buildTotalsMap(); + + List instructorDisplayOrder = Arrays.asList( + InstructorType.LADDER_FACULTY, + InstructorType.NEW_FACULTY_HIRE, + InstructorType.LECTURER_SOE, + InstructorType.CONTINUING_LECTURER, + InstructorType.EMERITI, + InstructorType.VISITING_PROFESSOR, + InstructorType.UNIT18_LECTURER, + InstructorType.CONTINUING_LECTURER_AUGMENTATION, + InstructorType.ASSOCIATE_INSTRUCTOR, + InstructorType.INSTRUCTOR); + + boolean firstInstructorTypeSection = true; + + for (InstructorType instructorType : instructorDisplayOrder) { + List assignments = assignmentsByInstructorType.get(instructorType); + + if (assignments.size() == 0) { + continue; + } + + Row row = worksheet.createRow(worksheet.getLastRowNum() + (firstInstructorTypeSection ? 0 : 1)); + + Cell cell = row.createCell(0); + cell.setCellValue(instructorType.getDescription().toUpperCase()); + cell.setCellType(CellType.STRING); + + ExcelHelper.writeRowToSheet(worksheet, instructorSectionHeaders); + + List instructorNames = + assignments.stream().map(WorkloadAssignment::getName).distinct().sorted().collect(Collectors.toList()); + + for (String name : instructorNames) { + List instructorAssignments = + assignments.stream().filter(a -> a.getName().equals(name)).collect( + Collectors.toList()); + + // placeholder named instructor without assignments + if (instructorAssignments.stream().anyMatch(assignment -> assignment.getTermCode() == null)) { + ExcelHelper.writeRowToSheet(worksheet, Collections.singletonList(instructorAssignments.get(0).getName())); + continue; + } + + Map instructorSubtotals = buildTotalsMap(); + + boolean namedRow = true; + for (WorkloadAssignment assignment : instructorAssignments) { + if (assignment.getName().equals("TBD")) { + updateTotals(placeholderTotals, assignment, true, false); + } else { + updateTotals(assignedTotals, assignment, namedRow, false); + } + + // count non-course assignments for instructor sub-totals + updateTotals(instructorSubtotals, assignment, namedRow, true); + + ExcelHelper.writeRowToSheet(worksheet, createInstructorRow(assignment, namedRow)); + namedRow = false; + } + + // instructor subtotal row + ExcelHelper.writeRowToSheet(worksheet, Arrays.asList( + "", "Totals", instructorSubtotals.get(Total.ASSIGNMENTS), "", + instructorSubtotals.get(Total.CENSUS) + " / " + instructorSubtotals.get(Total.PLANNED_SEATS), + instructorSubtotals.get(Total.PREVIOUS_ENROLLMENT), + instructorSubtotals.get(Total.LAST_OFFERED_ENROLLMENT), + instructorSubtotals.get(Total.UNITS), instructorSubtotals.get(Total.SCH))); + } + + ExcelHelper.writeRowToSheet(worksheet, Collections.singletonList("")); + + firstInstructorTypeSection = false; + } + + ExcelHelper.writeRowToSheet(worksheet, Collections.singletonList("UNASSIGNED COURSES")); + ExcelHelper.writeRowToSheet(worksheet, + Arrays.asList("Term", "Description", "Offering", "Enrollment / Seats", "Previous Enrollment", "Units", + "SCH")); + + // Unassigned section + List unassignedAssignments = + workloadAssignments.stream().filter(a -> a.getName().isEmpty()).collect( + Collectors.toList()); + + for (WorkloadAssignment unassignedAssignment : unassignedAssignments) { + updateTotals(unassignedTotals, unassignedAssignment, true, false); + + ExcelHelper.writeRowToSheet(worksheet, createUnassignedRow(unassignedAssignment)); + } + + ExcelHelper.writeRowToSheet(worksheet, Collections.singletonList("Totals")); + ExcelHelper.writeRowToSheet(worksheet, Collections.singletonList("")); + + // Summary Table + ExcelHelper.writeRowToSheet(worksheet, Collections.singletonList("ASSIGNMENT TOTALS")); + ExcelHelper.writeRowToSheet(worksheet, + Arrays.asList("Totals", "Instructor", "Assignments", "Enrollment / Seats", "Previous Enrollment", "Units", + "SCH")); + ExcelHelper.writeRowToSheet(worksheet, + Arrays.asList("Assigned", assignedTotals.get(Total.INSTRUCTOR_COUNT), + assignedTotals.get(Total.ASSIGNMENTS), + assignedTotals.get(Total.CENSUS) + " / " + assignedTotals.get(Total.PLANNED_SEATS), + assignedTotals.get(Total.PREVIOUS_ENROLLMENT), + assignedTotals.get(Total.UNITS), + assignedTotals.get(Total.SCH))); + ExcelHelper.writeRowToSheet(worksheet, + Arrays.asList("Unassigned", 0, + unassignedTotals.get(Total.ASSIGNMENTS), + unassignedTotals.get(Total.CENSUS) + " / " + unassignedTotals.get(Total.PLANNED_SEATS), + unassignedTotals.get(Total.PREVIOUS_ENROLLMENT), + unassignedTotals.get(Total.UNITS), + unassignedTotals.get(Total.SCH))); + ExcelHelper.writeRowToSheet(worksheet, + Arrays.asList("TBD Instructors", placeholderTotals.get(Total.INSTRUCTOR_COUNT), + placeholderTotals.get(Total.ASSIGNMENTS), + placeholderTotals.get(Total.CENSUS) + " / " + placeholderTotals.get(Total.PLANNED_SEATS), + placeholderTotals.get(Total.PREVIOUS_ENROLLMENT), + placeholderTotals.get(Total.UNITS), + placeholderTotals.get(Total.SCH))); + ExcelHelper.writeRowToSheet(worksheet, + Arrays.asList("Totals", + assignedTotals.get(Total.INSTRUCTOR_COUNT).intValue() + + unassignedTotals.get(Total.INSTRUCTOR_COUNT).intValue() + + placeholderTotals.get(Total.INSTRUCTOR_COUNT).intValue(), + assignedTotals.get(Total.ASSIGNMENTS).intValue() + unassignedTotals.get(Total.ASSIGNMENTS).intValue() + + placeholderTotals.get(Total.ASSIGNMENTS).intValue(), + assignedTotals.get(Total.CENSUS).intValue() + + unassignedTotals.get(Total.CENSUS).intValue() + + placeholderTotals.get(Total.CENSUS).intValue() + " / " + + assignedTotals.get(Total.PLANNED_SEATS).intValue() + + unassignedTotals.get(Total.PLANNED_SEATS).intValue() + + placeholderTotals.get(Total.PLANNED_SEATS).intValue(), + assignedTotals.get(Total.PREVIOUS_ENROLLMENT).intValue() + + unassignedTotals.get(Total.PREVIOUS_ENROLLMENT).intValue() + + placeholderTotals.get(Total.PREVIOUS_ENROLLMENT).intValue(), + assignedTotals.get(Total.UNITS).intValue() + unassignedTotals.get(Total.UNITS).intValue() + + placeholderTotals.get(Total.UNITS).intValue(), + assignedTotals.get(Total.SCH).floatValue() + unassignedTotals.get(Total.SCH).floatValue() + + placeholderTotals.get(Total.SCH).floatValue())); + + // header not on first row, need to offset + Row row = worksheet.getRow(worksheet.getFirstRowNum() + 1); + Iterator cellIterator = row.cellIterator(); + while (cellIterator.hasNext()) { + Cell cell = cellIterator.next(); + int columnIndex = cell.getColumnIndex(); + worksheet.autoSizeColumn(columnIndex); + } + } + + private void updateTotals(Map totalsMap, WorkloadAssignment assignment, boolean includeInstructor, boolean includeAssignment) { + int instructorCount = includeInstructor ? 1 : 0; + int assignmentCount = includeAssignment ? 1 : assignment.getOffering() == null ? 0 : 1; + long census = Optional.ofNullable(assignment.getCensus()).orElse(0L); + int plannedSeats = Optional.ofNullable(assignment.getPlannedSeats()).orElse(0); + + int units; + try { + units = Optional.ofNullable(assignment.getUnits()).map(Integer::parseInt).orElse(0); + } catch (NumberFormatException e) { + // variable unit courses have "1-4" as value, swallow exception and use 0 + units = 0; + } + + float sch = Optional.ofNullable(assignment.getStudentCreditHours()).orElse(0f); + long previousEnrollment = + Optional.ofNullable(assignment.getPreviousYearCensus()).orElse(0L); + int lastOfferedEnrollment = Optional.ofNullable(assignment.getLastOfferedCensus()).map( + str -> Integer.parseInt(str.substring(0, str.indexOf(' ')).replaceAll("[^0-9]", ""))) + .orElse(0); + + totalsMap.put(Total.INSTRUCTOR_COUNT, totalsMap.get(Total.INSTRUCTOR_COUNT).intValue() + instructorCount); + totalsMap.put(Total.ASSIGNMENTS, totalsMap.get(Total.ASSIGNMENTS).intValue() + assignmentCount); + totalsMap.put(Total.CENSUS, totalsMap.get(Total.CENSUS).longValue() + census); + totalsMap.put(Total.PLANNED_SEATS, totalsMap.get(Total.PLANNED_SEATS).intValue() + plannedSeats); + totalsMap.put(Total.PREVIOUS_ENROLLMENT, totalsMap.get(Total.PREVIOUS_ENROLLMENT).longValue() + previousEnrollment); + totalsMap.put(Total.LAST_OFFERED_ENROLLMENT, totalsMap.get(Total.LAST_OFFERED_ENROLLMENT).intValue() + lastOfferedEnrollment); + totalsMap.put(Total.UNITS, totalsMap.get(Total.UNITS).intValue() + units); + totalsMap.put(Total.SCH, totalsMap.get(Total.SCH).floatValue() + sch); + } + @Override protected void buildExcelDocument(Map model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) { - - String filename = "attachment; filename=\"" + this.year + " Workload Summary Report.xlsx"; + String baseName = this.snapshotName != null ? "Workload Snapshot - " + this.snapshotName : + this.year + " Workload Summary Report"; + String filename = "attachment; filename=\"" + baseName + ".xlsx"; response.setHeader("Content-Type", "multipart/mixed; charset=\"UTF-8\""); response.setHeader("Content-Disposition", filename); - buildRawAssignmentsSheet(workbook, instructorAssignmentList); + buildReportSheet(workbook, workloadAssignments); + buildRawAssignmentsSheet(workbook, workloadAssignments); ExcelHelper.expandHeaders(workbook); } + + private Map> generateInstructorTypeAssignmentsMap( + List workloadAssignments) { + Map> assignmentsByInstructorType = + buildInstructorTypesAssignmentsMap(); + + Set instructorTypes = assignmentsByInstructorType.keySet(); + + for (InstructorType instructorType : instructorTypes) { + List instructorTypeAssignments = + workloadAssignments.stream() + .filter(assignment -> instructorType.getDescription().equals(assignment.getInstructorType())) + .sorted(Comparator.comparing(WorkloadAssignment::getName) + .thenComparing(WorkloadAssignment::getTermCode).thenComparing(WorkloadAssignment::getDescription)).collect( + Collectors.toList()); + + assignmentsByInstructorType.put(instructorType, instructorTypeAssignments); + } + + return assignmentsByInstructorType; + } + + private List createInstructorRow(WorkloadAssignment assignment, boolean namedRow) { + String name = namedRow ? assignment.getName() : ""; + String enrollmentSeats = + assignment.getCensus() != null ? assignment.getCensus() + " / " + assignment.getPlannedSeats() : ""; + + int units; + try { + units = Optional.ofNullable(assignment.getUnits()).map(Integer::parseInt).orElse(0); + } catch (NumberFormatException e) { + // variable unit courses have "1-4" as value, swallow exception and use 0 + units = 0; + } + long previousYearCensus = 0L; + float studentCreditHours = 0f; + + if (assignment.getOffering() != null) { + previousYearCensus = assignment.getPreviousYearCensus() != null ? assignment.getPreviousYearCensus() : 0L; + studentCreditHours = assignment.getStudentCreditHours() != null ? assignment.getStudentCreditHours() : 0f; + } + + return Arrays.asList( + name, + Term.getFullName(assignment.getTermCode()), + assignment.getDescription(), + assignment.getOffering(), + enrollmentSeats, + previousYearCensus, + assignment.getLastOfferedCensus(), + units, + studentCreditHours, + assignment.getInstructorNote() + ); + } + + private List createUnassignedRow(WorkloadAssignment assignment) { + String enrollmentSeats = + assignment.getCensus() != null ? assignment.getCensus() + " / " + assignment.getPlannedSeats() : ""; + + return Arrays.asList( + Term.getFullName(assignment.getTermCode()), + assignment.getDescription(), + assignment.getOffering(), + enrollmentSeats, + assignment.getPreviousYearCensus(), + assignment.getUnits(), + assignment.getStudentCreditHours() + ); + } + + private Map buildTotalsMap() { + Map totalMap = new HashMap<>(); + totalMap.put(Total.INSTRUCTOR_COUNT, 0); + totalMap.put(Total.ASSIGNMENTS, 0); + totalMap.put(Total.CENSUS, 0); + totalMap.put(Total.PLANNED_SEATS, 0); + totalMap.put(Total.PREVIOUS_ENROLLMENT, 0); + totalMap.put(Total.LAST_OFFERED_ENROLLMENT, 0); + totalMap.put(Total.UNITS, 0); + totalMap.put(Total.SCH, 0); + return totalMap; + } + + private enum Total { + INSTRUCTOR_COUNT, ASSIGNMENTS, CENSUS, PLANNED_SEATS, PREVIOUS_ENROLLMENT, LAST_OFFERED_ENROLLMENT, UNITS, SCH + } + + private Map> buildInstructorTypesAssignmentsMap() { + Map> instructorTypeAssignments = new HashMap<>(); + + instructorTypeAssignments.put(InstructorType.LADDER_FACULTY, null); + instructorTypeAssignments.put(InstructorType.NEW_FACULTY_HIRE, null); + instructorTypeAssignments.put(InstructorType.LECTURER_SOE, null); + instructorTypeAssignments.put(InstructorType.CONTINUING_LECTURER, null); + instructorTypeAssignments.put(InstructorType.EMERITI, null); + instructorTypeAssignments.put(InstructorType.VISITING_PROFESSOR, null); + instructorTypeAssignments.put(InstructorType.UNIT18_LECTURER, null); + instructorTypeAssignments.put(InstructorType.CONTINUING_LECTURER_AUGMENTATION, null); + instructorTypeAssignments.put(InstructorType.ASSOCIATE_INSTRUCTOR, null); + instructorTypeAssignments.put(InstructorType.INSTRUCTOR, null); + + return instructorTypeAssignments; + } } diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/JpaWorkloadSummaryReportViewFactory.java b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/JpaWorkloadSummaryReportViewFactory.java index ea451f6b..860bde2c 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/JpaWorkloadSummaryReportViewFactory.java +++ b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/JpaWorkloadSummaryReportViewFactory.java @@ -1,43 +1,24 @@ package edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.factories; -import edu.ucdavis.dss.dw.dto.DwCensus; -import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.InstructorAssignment; import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.WorkloadHistoricalReportExcelView; import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.WorkloadSummaryReportExcelView; -import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.WorkloadSummaryReportView; -import edu.ucdavis.dss.ipa.entities.Course; -import edu.ucdavis.dss.ipa.entities.Instructor; -import edu.ucdavis.dss.ipa.entities.InstructorType; import edu.ucdavis.dss.ipa.entities.Schedule; -import edu.ucdavis.dss.ipa.entities.ScheduleInstructorNote; -import edu.ucdavis.dss.ipa.entities.Section; -import edu.ucdavis.dss.ipa.entities.SectionGroup; -import edu.ucdavis.dss.ipa.entities.TeachingAssignment; -import edu.ucdavis.dss.ipa.entities.Term; -import edu.ucdavis.dss.ipa.entities.UserRole; -import edu.ucdavis.dss.ipa.repositories.DataWarehouseRepository; -import edu.ucdavis.dss.ipa.services.CourseService; -import edu.ucdavis.dss.ipa.services.InstructorService; -import edu.ucdavis.dss.ipa.services.InstructorTypeService; -import edu.ucdavis.dss.ipa.services.ScheduleInstructorNoteService; +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; import edu.ucdavis.dss.ipa.services.ScheduleService; -import edu.ucdavis.dss.ipa.services.SectionGroupService; -import edu.ucdavis.dss.ipa.services.SectionService; -import edu.ucdavis.dss.ipa.services.TeachingAssignmentService; -import edu.ucdavis.dss.ipa.services.UserRoleService; +import edu.ucdavis.dss.ipa.services.WorkloadAssignmentService; +import edu.ucdavis.dss.ipa.services.WorkloadSnapshotService; import edu.ucdavis.dss.ipa.utilities.ExcelHelper; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; import javax.inject.Inject; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; @@ -45,115 +26,35 @@ @Service public class JpaWorkloadSummaryReportViewFactory implements WorkloadSummaryReportViewFactory { - @Inject - DataWarehouseRepository dwRepository; @Inject ScheduleService scheduleService; + @Inject - CourseService courseService; - @Inject - InstructorTypeService instructorTypeService; - @Inject - InstructorService instructorService; - @Inject - TeachingAssignmentService teachingAssignmentService; - @Inject - ScheduleInstructorNoteService scheduleInstructorNoteService; - @Inject - SectionGroupService sectionGroupService; - @Inject - SectionService sectionService; + WorkloadAssignmentService workloadAssignmentService; + @Inject - UserRoleService userRoleService; + WorkloadSnapshotService workloadSnapshotService; @Override - public WorkloadSummaryReportView createWorkloadSummaryReportView(long workgroupId, long year) { - Schedule schedule = scheduleService.findByWorkgroupIdAndYear(workgroupId, year); - - if (schedule == null) { - return null; - } - - List courses = schedule.getCourses(); - List sectionGroups = sectionGroupService.findByScheduleId(schedule.getId()); - List
sections = sectionService.findVisibleByWorkgroupIdAndYear(workgroupId, year); - List scheduleInstructorNotes = - scheduleInstructorNoteService.findByScheduleId(schedule.getId()); - List teachingAssignments = teachingAssignmentService.findApprovedByWorkgroupIdAndYear(workgroupId, year); - List instructorTypes = instructorTypeService.getAllInstructorTypes(); - - Set instructorSet = new HashSet<>(); - Set activeInstructors = - new HashSet<>(instructorService.findActiveByWorkgroupId(schedule.getWorkgroup().getId())); - Set assignedInstructors = - new HashSet<>(instructorService.findAssignedByScheduleId(schedule.getId())); - - instructorSet.addAll(assignedInstructors); - instructorSet.addAll(activeInstructors); - - List instructors = new ArrayList<>(instructorSet); - List termCodes = new ArrayList<>(); + public WorkloadSummaryReportExcelView createWorkloadSummaryReportExcelView(long workloadSnapshotId) { + WorkloadSnapshot snapshot = workloadSnapshotService.findById(workloadSnapshotId); - // TODO: remove termCodes not in Schedule - termCodes.addAll(Term.getTermCodesByYear(year)); - termCodes.addAll(Term.getTermCodesByYear(year - 1)); - - // SubjectCode: [CourseNumbers] - Map> courseMap = new HashMap<>(); - - for (Course c : courses) { - if (courseMap.containsKey(c.getSubjectCode()) == false) { - courseMap.put(c.getSubjectCode(), new ArrayList<>()); - } - - courseMap.get(c.getSubjectCode()).add(c.getCourseNumber()); - } - - List>> termCodeCensusFutures = new ArrayList<>(); - List>> courseCensusFutures = new ArrayList<>(); - - for (String subjectCode : courseMap.keySet()) { - termCodeCensusFutures.addAll(termCodes.stream().map(termCode -> CompletableFuture.supplyAsync( - () -> dwRepository.getCensusBySubjectCodeAndTermCode(subjectCode, termCode))).collect( - Collectors.toList())); - - courseCensusFutures.addAll(courseMap.get(subjectCode).stream().map( - courseNumber -> CompletableFuture.supplyAsync( - () -> dwRepository.getCensusBySubjectCodeAndCourseNumber(subjectCode, courseNumber))).collect( - Collectors.toList())); - } - - List termCodeCensus = - termCodeCensusFutures.stream().map(CompletableFuture::join).flatMap(Collection::stream) - .filter(c -> "CURRENT".equals(c.getSnapshotCode())).collect(Collectors.toList()); - List courseCensus = - courseCensusFutures.stream().map(CompletableFuture::join).flatMap(Collection::stream) - .filter(c -> "CURRENT".equals(c.getSnapshotCode())).collect(Collectors.toList()); - - Map> termCodeCensusMap = generateCensusMap(termCodeCensus); - Map> courseCensusMap = generateCensusMap(courseCensus); - - return new WorkloadSummaryReportView(year, schedule, courses, instructors, instructorTypes, teachingAssignments, - scheduleInstructorNotes, sectionGroups, sections, termCodeCensusMap, courseCensusMap); + return new WorkloadSummaryReportExcelView(snapshot.getWorkloadAssignments(), snapshot.getYear(), snapshot.getName()); } @Override public WorkloadSummaryReportExcelView createWorkloadSummaryReportExcelView(long[] workgroupIds, long year) { - List instructorAssignments = new ArrayList<>(); + List workloadAssignments = new ArrayList<>(); for (long workgroupId : workgroupIds) { - instructorAssignments.addAll(generateInstructorData(workgroupId, year)); + workloadAssignments.addAll(workloadAssignmentService.generateWorkloadAssignments(workgroupId, year, true)); } - // write data to excel - WorkloadSummaryReportExcelView workloadSummaryReportExcelView = - new WorkloadSummaryReportExcelView(instructorAssignments, year); - - return workloadSummaryReportExcelView; + return new WorkloadSummaryReportExcelView(workloadAssignments, year); } public WorkloadHistoricalReportExcelView createHistoricalWorkloadExcelView(long workgroupId, long year) { - Map> instructorAssignmentsMap = new HashMap<>(); + Map> workloadAssignmentsMap = new HashMap<>(); for (long i = 0; i < 5; i++) { long slotYear = year - i; @@ -162,17 +63,14 @@ public WorkloadHistoricalReportExcelView createHistoricalWorkloadExcelView(long // skip years without a schedule if (schedule != null) { - List instructorAssignments = new ArrayList<>(); - - instructorAssignments.addAll(generateInstructorData(workgroupId, slotYear)); + List workloadAssignments = + new ArrayList<>(workloadAssignmentService.generateWorkloadAssignments(workgroupId, slotYear)); - instructorAssignmentsMap.put(slotYear, instructorAssignments); + workloadAssignmentsMap.put(slotYear, workloadAssignments); } } - WorkloadHistoricalReportExcelView workloadHistoricalReportExcelView = new WorkloadHistoricalReportExcelView(instructorAssignmentsMap); - - return workloadHistoricalReportExcelView; + return new WorkloadHistoricalReportExcelView(workloadAssignmentsMap); } @Override @@ -180,7 +78,7 @@ public WorkloadHistoricalReportExcelView createHistoricalWorkloadExcelView(long @Transactional // needed for Async https://stackoverflow.com/questions/17278385/spring-async-generates-lazyinitializationexceptions public CompletableFuture createWorkloadSummaryReportBytes(long[] workgroupIds, long year) { - List instructorDTOList = new ArrayList<>(); + List workloadAssignments = new ArrayList<>(); System.out.println("Generating workload report for " + workgroupIds.length + " departments"); int count = 0; @@ -188,13 +86,13 @@ public CompletableFuture createWorkloadSummaryReportBytes(long[] workgro ++count; System.out.println(count + ". Generating for workgroupId: " + workgroupId); - instructorDTOList.addAll(generateInstructorData(workgroupId, year)); + workloadAssignments.addAll(workloadAssignmentService.generateWorkloadAssignments(workgroupId, year)); } System.out.println("Finished gathering data, writing to excel"); XSSFWorkbook workbook = new XSSFWorkbook(); - WorkloadSummaryReportExcelView.buildRawAssignmentsSheet(workbook, instructorDTOList); + WorkloadSummaryReportExcelView.buildRawAssignmentsSheet(workbook, workloadAssignments); ExcelHelper.expandHeaders(workbook); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -208,205 +106,51 @@ public CompletableFuture createWorkloadSummaryReportBytes(long[] workgro return CompletableFuture.completedFuture(bos.toByteArray()); } - private List generateInstructorData(long workgroupId, long year) { - List instructorAssignments = new ArrayList<>(); + @Override + @Transactional + public CompletableFuture createWorkloadSummaryReportBytes(Map> departmentSnapshots, long year) { + Instant start = Instant.now(); - WorkloadSummaryReportView workloadSummaryReportView = createWorkloadSummaryReportView(workgroupId, year); + Set>> entries = departmentSnapshots.entrySet(); - String department = workloadSummaryReportView.getSchedule().getWorkgroup().getName(); + List workloadAssignments = new ArrayList<>(); + System.out.println("Generating workload report for " + entries.size() + " departments"); - for (Instructor instructor : workloadSummaryReportView.getInstructors()) { - Long instructorTypeId = - getInstructorTypeId(instructor, workloadSummaryReportView.getTeachingAssignment(), workgroupId); + int count = 0; + for (Map.Entry> department : entries) { + ++count; - if (instructorTypeId == null) { - continue; - } + long workgroupId = department.getKey(); + System.out.println(count + ". Generating for workgroupId: " + workgroupId); - String instructorTypeDescription = instructorTypeService.findById(instructorTypeId).getDescription(); - - List scheduleAssignments = workloadSummaryReportView.getTeachingAssignment().stream() - .filter(ta -> ta.getInstructor() != null && ta.getInstructor().getId() == instructor.getId()) - .collect(Collectors.toList()); - - String instructorNote = workloadSummaryReportView.getScheduleInstructorNotes().stream() - .filter(note -> note.getInstructor().getId() == instructor.getId()) - .map(note -> note.getInstructorComment()).findAny().orElse(""); - - if (scheduleAssignments.size() == 0) { - instructorAssignments.add( - new InstructorAssignment(year, department, instructorTypeDescription, instructor.getFullName(), instructorNote)); - } else { - for (TeachingAssignment assignment : scheduleAssignments) { - String termCode = assignment.getTermCode(); - String previousYearTermCode = - Integer.parseInt(termCode.substring(0, 4)) - 1 + termCode.substring(4, 6); - - String courseDescription = null, offering = null, lastOfferedCensus = null, unit = null; - Integer plannedSeats = null; - long censusCount = 0; - long previousYearCensus = 0; - Float studentCreditHour = null; - - String courseType = getCourseType(assignment); - if (assignment.getSectionGroup() != null) { - SectionGroup sectionGroup = assignment.getSectionGroup(); - Course course = sectionGroup.getCourse(); - - courseDescription = course.getSubjectCode() + " " + course.getCourseNumber(); - offering = course.getSequencePattern(); - unit = sectionGroup.getDisplayUnits(); - - if (workloadSummaryReportView.getTermCodeCensus().size() > 0) { - String courseKey = course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + - course.getSequencePattern(); - Map> censusMap = - workloadSummaryReportView.getTermCodeCensus(); - - if (censusMap.containsKey(termCode) && censusMap.get(termCode).containsKey(courseKey)) { - censusCount = censusMap.get(termCode).get(courseKey); - - studentCreditHour = calculateStudentCreditHours(censusCount, course, sectionGroup); - plannedSeats = sectionGroup.getPlannedSeats(); - } - - if (censusMap.containsKey(previousYearTermCode) && - censusMap.get(previousYearTermCode).containsKey(courseKey)) { - previousYearCensus = censusMap.get(previousYearTermCode).get(courseKey); - } - - // find last offering term - Map> courseCensusMap = - workloadSummaryReportView.getCourseCensus(); - - List offeredTermCodes = courseCensusMap.keySet().stream().sorted( - Comparator.naturalOrder()).collect( - Collectors.toList()); - - String lastOfferedTermCode; - String lastOfferedCourseKey = - course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + - course.getSequencePattern(); - if (offeredTermCodes.size() > 2) { - lastOfferedTermCode = offeredTermCodes.get(offeredTermCodes.size() - 2); - - if (courseCensusMap.get(lastOfferedTermCode) - .containsKey(lastOfferedCourseKey)) { - lastOfferedCensus = - courseCensusMap.get(lastOfferedTermCode).get(lastOfferedCourseKey) + - " (" + - Term.getShortDescription(lastOfferedTermCode) + ")"; - } - } else { - lastOfferedCensus = ""; - } - } - } else { - courseDescription = assignment.getDescription(); - } - - instructorAssignments.add(new InstructorAssignment(year, department, instructorTypeDescription, - instructor.getLastName() + ", " + instructor.getFirstName(), - Term.getRegistrarName(termCode) + " " + Term.getYear(termCode), courseType, - courseDescription, offering, censusCount, plannedSeats, previousYearCensus, - lastOfferedCensus, unit, studentCreditHour, instructorNote)); - } + if (department.getValue().size() == 1) { + // only 1 snapshot selected, include live data + workloadAssignments.addAll(workloadAssignmentService.generateWorkloadAssignments(workgroupId, year)); } - } - - // fill in TBD instructor assignments - List unnamedAssignments = workloadSummaryReportView.getTeachingAssignment().stream() - .filter(teachingAssignment -> teachingAssignment.getInstructor() == null).collect( - Collectors.toList()); - for (TeachingAssignment teachingAssignment : unnamedAssignments) { - instructorAssignments.add( - new InstructorAssignment(year, department, - instructorTypeService.findById(teachingAssignment.getInstructorTypeIdentification()) - .getDescription(), "TBD", - Term.getRegistrarName(teachingAssignment.getTermCode()), getCourseType(teachingAssignment), - teachingAssignment.getDescription(), - teachingAssignment.getSectionGroup().getCourse().getSequencePattern())); - } - return instructorAssignments; - } - - private Long getInstructorTypeId(Instructor instructor, List teachingAssignments, - long workgroupId) { - // attempt by userRole - UserRole userRole = - userRoleService.findOrCreateByLoginIdAndWorkgroupIdAndRoleToken(instructor.getLoginId(), workgroupId, - "instructor"); - - if (userRole != null) { - return userRole.getInstructorTypeIdentification(); - } - // attempt by teachingAssignment - TeachingAssignment teachingAssignment = - teachingAssignments.stream().filter(ta -> ta.getInstructor().getId() == instructor.getId()).findFirst() - .get(); - - if (teachingAssignment != null) { - return teachingAssignment.getInstructorType().getId(); - } - return null; - } - private String getCourseType(TeachingAssignment teachingAssignment) { - SectionGroup sectionGroup = teachingAssignment.getSectionGroup(); - - if (sectionGroup == null) { - // non-Course Assignment - return teachingAssignment.getDescription(); - } else { - Course course = teachingAssignment.getSectionGroup().getCourse(); - int courseNumbers = Integer.parseInt(course.getCourseNumber().replaceAll("[^\\d.]", "")); - - if (courseNumbers < 100) { - return "Lower"; - } else if (courseNumbers >= 200) { - return "Grad"; - } else { - return "Upper"; + for (long snapshotId : department.getValue()) { + workloadAssignments.addAll(workloadAssignmentService.findByWorkloadSnapshotId(snapshotId)); } } - } - private Float calculateStudentCreditHours(Long students, Course course, SectionGroup sectionGroup) { - Float units = 0.0f; + Instant end = Instant.now(); + System.out.println("Finished gathering assignments in " + Duration.between(start, end).toMinutes() + " minutes"); - if (sectionGroup.getUnitsVariable() != null) { - units = sectionGroup.getUnitsVariable(); - } else if (course.getUnitsLow() != null && course.getUnitsLow() > 0) { - units = course.getUnitsLow(); - } + System.out.println("Finished gathering data, writing to excel"); - return students * units; - } + XSSFWorkbook workbook = new XSSFWorkbook(); + WorkloadSummaryReportExcelView.buildRawAssignmentsSheet(workbook, workloadAssignments); + ExcelHelper.expandHeaders(workbook); - /** - * @param censuses - * @return { "termCode": { "course": enrollmentCount } } - */ - private Map> generateCensusMap(List censuses) { - Map> censusMap = new HashMap<>(); - for (DwCensus census : censuses) { - String termCode = census.getTermCode(); - String courseKey = - census.getSubjectCode() + "-" + census.getCourseNumber() + "-" + census.getSequencePattern(); - - if (censusMap.get(termCode) == null) { - censusMap.put(termCode, new HashMap<>()); - } + ByteArrayOutputStream bos = new ByteArrayOutputStream(); - if (censusMap.get(termCode).get(courseKey) == null) { - censusMap.get(termCode).put(courseKey, census.getCurrentEnrolledCount()); - } else { - censusMap.get(termCode) - .put(courseKey, censusMap.get(termCode).get(courseKey) + census.getCurrentEnrolledCount()); - } + try { + workbook.write(bos); + } catch (IOException e) { + e.printStackTrace(); } - return censusMap; + return CompletableFuture.completedFuture(bos.toByteArray()); } } diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/WorkloadSummaryReportViewFactory.java b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/WorkloadSummaryReportViewFactory.java index 2ffdba94..ea3691c3 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/WorkloadSummaryReportViewFactory.java +++ b/src/main/java/edu/ucdavis/dss/ipa/api/components/workloadSummaryReport/views/factories/WorkloadSummaryReportViewFactory.java @@ -3,14 +3,17 @@ import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.WorkloadHistoricalReportExcelView; import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.WorkloadSummaryReportExcelView; import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.WorkloadSummaryReportView; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; public interface WorkloadSummaryReportViewFactory { - WorkloadSummaryReportView createWorkloadSummaryReportView(long workgroupId, long year); - - WorkloadHistoricalReportExcelView createHistoricalWorkloadExcelView(long workgroupId, long year); + WorkloadSummaryReportExcelView createWorkloadSummaryReportExcelView(long workloadSnapshotId); WorkloadSummaryReportExcelView createWorkloadSummaryReportExcelView(long[] workgroupId, long year); + WorkloadHistoricalReportExcelView createHistoricalWorkloadExcelView(long workgroupId, long year); + CompletableFuture createWorkloadSummaryReportBytes(long[] workgroupId, long year); + CompletableFuture createWorkloadSummaryReportBytes(Map> departmentSnapshots, long year); } diff --git a/src/main/java/edu/ucdavis/dss/ipa/api/entities/WorkloadSnapshotController.java b/src/main/java/edu/ucdavis/dss/ipa/api/entities/WorkloadSnapshotController.java new file mode 100644 index 00000000..84af5ce5 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/api/entities/WorkloadSnapshotController.java @@ -0,0 +1,122 @@ +package edu.ucdavis.dss.ipa.api.entities; + +import edu.ucdavis.dss.ipa.api.components.workloadSummaryReport.views.factories.WorkloadSummaryReportViewFactory; +import edu.ucdavis.dss.ipa.entities.Workgroup; +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; +import edu.ucdavis.dss.ipa.security.Authorization; +import edu.ucdavis.dss.ipa.security.Authorizer; +import edu.ucdavis.dss.ipa.security.UrlEncryptor; +import edu.ucdavis.dss.ipa.services.UserService; +import edu.ucdavis.dss.ipa.services.WorkgroupService; +import edu.ucdavis.dss.ipa.services.WorkloadSnapshotService; +import java.text.ParseException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.View; + +@RestController +public class WorkloadSnapshotController { + @Inject + UserService userService; + + @Inject + WorkgroupService workgroupService; + + @Inject + Authorizer authorizer; + + @Inject + Authorization authorization; + + @Inject + WorkloadSnapshotService workloadSnapshotService; + + @Inject + WorkloadSummaryReportViewFactory workloadSummaryReportViewFactory; + + @Value("${IPA_URL_API}") + String ipaUrlApi; + + @Value("${IPA_URL_FRONTEND}") + String ipaUrlFrontend; + + @RequestMapping(value = "/api/workgroups/{workgroupId}/years/{year}/workloadSnapshots", method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public List getWorkloadSnapshots(@PathVariable long workgroupId, @PathVariable long year, + HttpServletResponse httpResponse) { + Workgroup workgroup = workgroupService.findOneById(workgroupId); + + if (workgroup == null) { + httpResponse.setStatus(HttpStatus.NOT_FOUND.value()); + return null; + } + + authorizer.hasWorkgroupRoles(workgroup.getId(), "academicPlanner", "reviewer"); + List workloadSnapshots = workloadSnapshotService.findByWorkgroupIdAndYear(workgroupId, year); + + return workloadSnapshots; + } + + @RequestMapping(value = "/api/workloadSnapshots/{workloadSnapshotId}/generateExcel", method = RequestMethod.GET) + @ResponseBody + public Map generateExcel(@PathVariable long workloadSnapshotId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + WorkloadSnapshot snapshot = workloadSnapshotService.findById(workloadSnapshotId); + + if (snapshot == null) { + httpResponse.setStatus(HttpStatus.NOT_FOUND.value()); + return null; + } + + long workgroupId = snapshot.getWorkgroup().getId(); + authorizer.hasWorkgroupRoles(workgroupId, "academicPlanner", "reviewer"); + + String url = + ipaUrlApi + "/download/workloadSnapshots/" + workloadSnapshotId + "/excel"; + String salt = RandomStringUtils.randomAlphanumeric(16).toUpperCase(); + + String ipAddress = httpRequest.getHeader("X-FORWARDED-FOR"); + if (ipAddress == null) { + ipAddress = httpRequest.getRemoteAddr(); + } + + Map map = new HashMap<>(); + map.put("redirect", url + "/" + salt + "/" + UrlEncryptor.encrypt(salt, ipAddress)); + + return map; + } + + @RequestMapping(value = "/download/workloadSnapshots/{workloadSnapshotId}/excel/{salt}/{encrypted}") + public View downloadExcel(@PathVariable long workloadSnapshotId, + @PathVariable String salt, @PathVariable String encrypted, + HttpServletRequest httpRequest, HttpServletResponse httpResponse) + throws ParseException { + long TIMEOUT = 30L; // In seconds + + String ipAddress = httpRequest.getHeader("X-FORWARDED-FOR"); + if (ipAddress == null) { + ipAddress = httpRequest.getRemoteAddr(); + } + + boolean isValidUrl = UrlEncryptor.validate(salt, encrypted, ipAddress, TIMEOUT); + + if (isValidUrl) { + return workloadSummaryReportViewFactory + .createWorkloadSummaryReportExcelView(workloadSnapshotId); + } else { + httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + return null; + } + } +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/entities/SectionGroup.java b/src/main/java/edu/ucdavis/dss/ipa/entities/SectionGroup.java index 5a1abae8..f19d16b5 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/entities/SectionGroup.java +++ b/src/main/java/edu/ucdavis/dss/ipa/entities/SectionGroup.java @@ -217,6 +217,11 @@ public String getDisplayUnits() { String format = this.getUnitsVariable() % 1 == 0 ? "%.0f" : "%.1f"; return String.format(format, this.getUnitsVariable()); } else if (course.getUnitsHigh() != null) { + if (course.getUnitsLow() == 0f && course.getUnitsHigh() == 0f) { + // Placeholder course + return String.valueOf(0); + } + String unitsHighFormat = course.getUnitsHigh() % 1 == 0 ? "%.0f" : "%.1f"; String unitsLowFormat = course.getUnitsLow() % 1 == 0 ? "%.0f" : "%.1f"; @@ -228,4 +233,14 @@ public String getDisplayUnits() { return null; } } + + @Transient + public String getDisplayUnits(Boolean isWorkload) { + // use 0 for variable courses for Workload/SCH purposes + if (isWorkload) { + return String.valueOf(0); + } else { + return getDisplayUnits(); + } + } } diff --git a/src/main/java/edu/ucdavis/dss/ipa/entities/Term.java b/src/main/java/edu/ucdavis/dss/ipa/entities/Term.java index b29f28fd..c83190ef 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/entities/Term.java +++ b/src/main/java/edu/ucdavis/dss/ipa/entities/Term.java @@ -216,7 +216,7 @@ static public String getRegistrarName(String termCode) { static public String getShortDescription(String termCode) { if(termCode == null) throw new IllegalArgumentException("termCode cannot be null"); - String year = getYear(termCode); + String year = getYear(termCode).substring(2); String term = termCode.length() == 2 ? termCode : termCode.substring(4); int code = Integer.parseInt(term); @@ -260,6 +260,23 @@ public static String getAcademicYear(String termCode) { @Transient public static String getAcademicYearFromYear(long year) { - return year + "-" + String.valueOf(year + 1).substring(2,4); - } + return year + "-" + String.valueOf(year + 1).substring(2,4); + } + + @Transient + public static String getFullName(String termCode) { + if (termCode == null) { + return null; + } + + return getRegistrarName(termCode) + " " + getYear(termCode); + } + + @Transient + public static String getPreviousYearTermCode(String termCode) { + if (termCode == null) throw new IllegalArgumentException("termCode cannot be null"); + if (termCode.length() != 6) throw new IllegalArgumentException("Cannot get previousYearTermCode if termCode is short"); + + return Integer.parseInt(termCode.substring(0, 4)) - 1 + termCode.substring(4, 6); + } } diff --git a/src/main/java/edu/ucdavis/dss/ipa/entities/WorkloadAssignment.java b/src/main/java/edu/ucdavis/dss/ipa/entities/WorkloadAssignment.java new file mode 100644 index 00000000..422706d9 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/entities/WorkloadAssignment.java @@ -0,0 +1,198 @@ +package edu.ucdavis.dss.ipa.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Arrays; +import java.util.List; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Transient; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; + +@Entity +@Table(name = "WorkloadAssignments") +public class WorkloadAssignment extends BaseEntity { + private long id; + private WorkloadSnapshot workloadSnapshot; + private String department, instructorType, name, termCode, courseType, description, offering, lastOfferedCensus, + units, + instructorNote; + private Long census, previousYearCensus; + private Integer plannedSeats; + private Float studentCreditHours; + private long year; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "Id", unique = true, nullable = false) + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "WorkloadSnapshotId", nullable = false) + @NotNull + @JsonIgnore + public WorkloadSnapshot getWorkloadSnapshot() { + return workloadSnapshot; + } + + public void setWorkloadSnapshot(WorkloadSnapshot workloadSnapshot) { + this.workloadSnapshot = workloadSnapshot; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } + + public String getInstructorType() { + return instructorType; + } + + public void setInstructorType(String instructorType) { + this.instructorType = instructorType; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTermCode() { + return termCode; + } + + public void setTermCode(String termCode) { + this.termCode = termCode; + } + + public String getCourseType() { + return courseType; + } + + public void setCourseType(String courseType) { + this.courseType = courseType; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getOffering() { + return offering; + } + + public void setOffering(String offering) { + this.offering = offering; + } + + public String getLastOfferedCensus() { + return lastOfferedCensus; + } + + public void setLastOfferedCensus(String lastOfferedCensus) { + this.lastOfferedCensus = lastOfferedCensus; + } + + public String getUnits() { + return units; + } + + public void setUnits(String units) { + this.units = units; + } + + public String getInstructorNote() { + return instructorNote; + } + + public void setInstructorNote(String instructorNote) { + this.instructorNote = instructorNote; + } + + public Long getCensus() { + return census; + } + + public void setCensus(Long census) { + this.census = census; + } + + public Long getPreviousYearCensus() { + return previousYearCensus; + } + + public void setPreviousYearCensus(Long previousYearCensus) { + this.previousYearCensus = previousYearCensus; + } + + public Integer getPlannedSeats() { + return plannedSeats; + } + + public void setPlannedSeats(Integer plannedSeats) { + this.plannedSeats = plannedSeats; + } + + public Float getStudentCreditHours() { + return studentCreditHours; + } + + public void setStudentCreditHours(Float studentCreditHours) { + this.studentCreditHours = studentCreditHours; + } + + public long getYear() { + return year; + } + + public void setYear(long year) { + this.year = year; + } + + @Transient + public List toList() { + String snapshotName = this.getWorkloadSnapshot() != null ? this.getWorkloadSnapshot().getName() : "Live Data"; + + return Arrays.asList( + this.year == 0 ? "" : this.year + "-" + String.valueOf(this.year + 1).substring(2, 4), + snapshotName, + this.department, + this.instructorType.toUpperCase(), + this.name, + Term.getFullName(this.termCode), + this.courseType, + this.description, + this.offering, + this.census, + this.plannedSeats, + this.previousYearCensus, + this.lastOfferedCensus, + this.units, + this.studentCreditHours, + this.instructorNote + ); + } +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/entities/WorkloadSnapshot.java b/src/main/java/edu/ucdavis/dss/ipa/entities/WorkloadSnapshot.java new file mode 100644 index 00000000..c2f3e513 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/entities/WorkloadSnapshot.java @@ -0,0 +1,92 @@ +package edu.ucdavis.dss.ipa.entities; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import java.util.List; +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +@Entity +@Table(name = "WorkloadSnapshots") +public class WorkloadSnapshot extends BaseEntity { + private long id; + private String name; + private BudgetScenario budgetScenario; + private Workgroup workgroup; + private long year; + private List workloadAssignments; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "Id", unique = true, nullable = false) + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "BudgetScenarioId", nullable = false) + @NotNull + @JsonIgnore + public BudgetScenario getBudgetScenario() { + return budgetScenario; + } + + public void setBudgetScenario(BudgetScenario budgetScenario) { + this.budgetScenario = budgetScenario; + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "WorkgroupId", nullable = false) + @NotNull + @JsonIgnore + public Workgroup getWorkgroup() { + return workgroup; + } + + public void setWorkgroup(Workgroup workgroup) { + this.workgroup = workgroup; + } + + @JsonProperty("workgroupId") + @Transient + public Long getWorkgroupIdentification() { + return this.workgroup != null ? this.workgroup.getId() : null; + } + + public long getYear() { + return year; + } + + public void setYear(long year) { + this.year = year; + } + + @JsonProperty("createdOn") + @Transient + public Date getCreatedOn() { + return createdAt; + } + + @OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "workloadSnapshot", cascade = {CascadeType.ALL}) + public List getWorkloadAssignments() { + return workloadAssignments; + } + + public void setWorkloadAssignments(List workloadAssignments) { + this.workloadAssignments = workloadAssignments; + } +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/entities/enums/InstructorDescription.java b/src/main/java/edu/ucdavis/dss/ipa/entities/enums/InstructorDescription.java deleted file mode 100644 index 21d8e1db..00000000 --- a/src/main/java/edu/ucdavis/dss/ipa/entities/enums/InstructorDescription.java +++ /dev/null @@ -1,24 +0,0 @@ -package edu.ucdavis.dss.ipa.entities.enums; - -public enum InstructorDescription { - EMERITI(1), - VISITING_PROFESSOR(2), - ASSOCIATE_PROFESSOR(3), - UNIT18_LECTURER(4), - CONTINUING_LECTURER(5), - LADDER_FACULTY(6), - INSTRUCTOR(7), - LECTURER_SOE(8), - NEW_FACULTY_HIRE(9), - CONTINUING_LECTURER_AUGMENTATION(10); - - private final long typeId; - - InstructorDescription (long typeId) { - this.typeId = typeId; - } - - public long typeId() { - return typeId; - } -} diff --git a/src/main/java/edu/ucdavis/dss/ipa/entities/enums/InstructorType.java b/src/main/java/edu/ucdavis/dss/ipa/entities/enums/InstructorType.java new file mode 100644 index 00000000..518a59df --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/entities/enums/InstructorType.java @@ -0,0 +1,30 @@ +package edu.ucdavis.dss.ipa.entities.enums; + +public enum InstructorType { + EMERITI(1, "Emeriti - Recalled"), + VISITING_PROFESSOR(2, "Visiting Professor"), + ASSOCIATE_INSTRUCTOR(3, "Associate Instructor"), + UNIT18_LECTURER(4, "Unit 18 Pre-Six Lecturer"), + CONTINUING_LECTURER(5, "Continuing Lecturer"), + LADDER_FACULTY(6, "Ladder Faculty"), + INSTRUCTOR(7, "Instructor"), + LECTURER_SOE(8, "Lecturer SOE"), + NEW_FACULTY_HIRE(9, "New Faculty Hire"), + CONTINUING_LECTURER_AUGMENTATION(10, "Continuing Lecturer - Augmentation"); + + private final long id; + private final String description; + + InstructorType(long id, String description) { + this.id = id; + this.description = description; + } + + public long getId() { + return id; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/repositories/WorkloadAssignmentRepository.java b/src/main/java/edu/ucdavis/dss/ipa/repositories/WorkloadAssignmentRepository.java new file mode 100644 index 00000000..e1cc2496 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/repositories/WorkloadAssignmentRepository.java @@ -0,0 +1,12 @@ +package edu.ucdavis.dss.ipa.repositories; + +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; +import java.util.List; +import org.springframework.data.repository.CrudRepository; + +public interface WorkloadAssignmentRepository extends CrudRepository { + WorkloadAssignment findById(long id); + + List findByWorkloadSnapshotId(long id); +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/repositories/WorkloadSnapshotRepository.java b/src/main/java/edu/ucdavis/dss/ipa/repositories/WorkloadSnapshotRepository.java new file mode 100644 index 00000000..986870d3 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/repositories/WorkloadSnapshotRepository.java @@ -0,0 +1,13 @@ +package edu.ucdavis.dss.ipa.repositories; + +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; +import java.util.List; +import org.springframework.data.repository.CrudRepository; + +public interface WorkloadSnapshotRepository extends CrudRepository { + WorkloadSnapshot findById(long id); + + List findByWorkgroupIdAndYear(long workgroupId, long year); + + WorkloadSnapshot findByBudgetScenarioId(long budgetScenarioId); +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/services/BudgetCalculationService.java b/src/main/java/edu/ucdavis/dss/ipa/services/BudgetCalculationService.java index 8ec30c6f..229c368d 100644 --- a/src/main/java/edu/ucdavis/dss/ipa/services/BudgetCalculationService.java +++ b/src/main/java/edu/ucdavis/dss/ipa/services/BudgetCalculationService.java @@ -1,7 +1,7 @@ package edu.ucdavis.dss.ipa.services; import static edu.ucdavis.dss.ipa.entities.enums.BudgetSummary.*; -import static edu.ucdavis.dss.ipa.entities.enums.InstructorDescription.*; +import static edu.ucdavis.dss.ipa.entities.enums.InstructorType.*; import edu.ucdavis.dss.ipa.entities.*; import edu.ucdavis.dss.ipa.entities.enums.BudgetSummary; @@ -95,61 +95,61 @@ public Map> calculateTermTotals(Budget bu if(sectionGroupCostInstructor.getTeachingAssignment() != null){ teachingAssignmentIds.add(sectionGroupCostInstructor.getTeachingAssignment().getId()); } - if(instructorTypeId == EMERITI.typeId()){ + if(instructorTypeId == EMERITI.getId()){ currentTermSummary.put(EMERITI_COUNT, currentTermSummary.get(EMERITI_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(EMERITI_COUNT, combinedTermSummary.get(EMERITI_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(EMERITI_COST, currentTermSummary.get(EMERITI_COST).add(instructorCost)); combinedTermSummary.put(EMERITI_COST, combinedTermSummary.get(EMERITI_COST).add(instructorCost)); - } else if (instructorTypeId == VISITING_PROFESSOR.typeId()){ + } else if (instructorTypeId == VISITING_PROFESSOR.getId()){ currentTermSummary.put(VISITING_PROFESSOR_COUNT, currentTermSummary.get(VISITING_PROFESSOR_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(VISITING_PROFESSOR_COUNT, combinedTermSummary.get(VISITING_PROFESSOR_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(VISITING_PROFESSOR_COST, currentTermSummary.get(VISITING_PROFESSOR_COST).add(instructorCost)); combinedTermSummary.put(VISITING_PROFESSOR_COST, combinedTermSummary.get(VISITING_PROFESSOR_COST).add(instructorCost)); - } else if (instructorTypeId == ASSOCIATE_PROFESSOR.typeId()){ + } else if (instructorTypeId == ASSOCIATE_INSTRUCTOR.getId()){ currentTermSummary.put(ASSOCIATE_INSTRUCTOR_COUNT, currentTermSummary.get(ASSOCIATE_INSTRUCTOR_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(ASSOCIATE_INSTRUCTOR_COUNT, combinedTermSummary.get(ASSOCIATE_INSTRUCTOR_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(ASSOCIATE_INSTRUCTOR_COST, currentTermSummary.get(ASSOCIATE_INSTRUCTOR_COST).add(instructorCost)); combinedTermSummary.put(ASSOCIATE_INSTRUCTOR_COST, combinedTermSummary.get(ASSOCIATE_INSTRUCTOR_COST).add(instructorCost)); - } else if (instructorTypeId == UNIT18_LECTURER.typeId()){ + } else if (instructorTypeId == UNIT18_LECTURER.getId()){ currentTermSummary.put(UNIT18_LECTURER_COUNT, currentTermSummary.get(UNIT18_LECTURER_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(UNIT18_LECTURER_COUNT, combinedTermSummary.get(UNIT18_LECTURER_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(UNIT18_LECTURER_COST, currentTermSummary.get(UNIT18_LECTURER_COST).add(instructorCost)); combinedTermSummary.put(UNIT18_LECTURER_COST, combinedTermSummary.get(UNIT18_LECTURER_COST).add(instructorCost)); - } else if (instructorTypeId == CONTINUING_LECTURER.typeId()){ + } else if (instructorTypeId == CONTINUING_LECTURER.getId()){ currentTermSummary.put(CONTINUING_LECTURER_COUNT, currentTermSummary.get(CONTINUING_LECTURER_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(CONTINUING_LECTURER_COUNT, combinedTermSummary.get(CONTINUING_LECTURER_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(CONTINUING_LECTURER_COST, currentTermSummary.get(CONTINUING_LECTURER_COST).add(instructorCost)); combinedTermSummary.put(CONTINUING_LECTURER_COST, combinedTermSummary.get(CONTINUING_LECTURER_COST).add(instructorCost)); - } else if (instructorTypeId == CONTINUING_LECTURER_AUGMENTATION.typeId()){ + } else if (instructorTypeId == CONTINUING_LECTURER_AUGMENTATION.getId()){ currentTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COUNT, currentTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COUNT, combinedTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COST, currentTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COST).add(instructorCost)); combinedTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COST, combinedTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COST).add(instructorCost)); - } else if (instructorTypeId == LADDER_FACULTY.typeId()){ + } else if (instructorTypeId == LADDER_FACULTY.getId()){ currentTermSummary.put(LADDER_FACULTY_COUNT, currentTermSummary.get(LADDER_FACULTY_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(LADDER_FACULTY_COUNT, combinedTermSummary.get(LADDER_FACULTY_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(LADDER_FACULTY_COST, currentTermSummary.get(LADDER_FACULTY_COST).add(instructorCost)); combinedTermSummary.put(LADDER_FACULTY_COST, combinedTermSummary.get(LADDER_FACULTY_COST).add(instructorCost)); - } else if (instructorTypeId == INSTRUCTOR.typeId()){ + } else if (instructorTypeId == INSTRUCTOR.getId()){ currentTermSummary.put(INSTRUCTOR_COUNT, currentTermSummary.get(INSTRUCTOR_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(INSTRUCTOR_COUNT, combinedTermSummary.get(INSTRUCTOR_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(INSTRUCTOR_COST, currentTermSummary.get(INSTRUCTOR_COST).add(instructorCost)); combinedTermSummary.put(INSTRUCTOR_COST, combinedTermSummary.get(INSTRUCTOR_COST).add(instructorCost)); - } else if (instructorTypeId == LECTURER_SOE.typeId()){ + } else if (instructorTypeId == LECTURER_SOE.getId()){ currentTermSummary.put(LECTURER_SOE_COUNT, currentTermSummary.get(LECTURER_SOE_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(LECTURER_SOE_COUNT, combinedTermSummary.get(LECTURER_SOE_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(LECTURER_SOE_COST, currentTermSummary.get(LECTURER_SOE_COST).add(instructorCost)); combinedTermSummary.put(LECTURER_SOE_COST, combinedTermSummary.get(LECTURER_SOE_COST).add(instructorCost)); - } else if (instructorTypeId == NEW_FACULTY_HIRE.typeId()){ + } else if (instructorTypeId == NEW_FACULTY_HIRE.getId()){ currentTermSummary.put(NEW_FACULTY_HIRE_COUNT, currentTermSummary.get(NEW_FACULTY_HIRE_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(NEW_FACULTY_HIRE_COUNT, combinedTermSummary.get(NEW_FACULTY_HIRE_COUNT).add(BigDecimal.ONE)); @@ -244,61 +244,61 @@ public Map> calculateTermTotals(Budget bu BigDecimal instructorCost = calculateTeachingAssignmentCost(workgroup, budget, teachingAssignment); long instructorTypeId = calculateTeachingAssignmentTypeId(teachingAssignment, workgroup); - if(instructorTypeId == EMERITI.typeId()){ + if(instructorTypeId == EMERITI.getId()){ currentTermSummary.put(EMERITI_COUNT, currentTermSummary.get(EMERITI_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(EMERITI_COUNT, combinedTermSummary.get(EMERITI_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(EMERITI_COST, currentTermSummary.get(EMERITI_COST).add(instructorCost)); combinedTermSummary.put(EMERITI_COST, combinedTermSummary.get(EMERITI_COST).add(instructorCost)); - } else if (instructorTypeId == VISITING_PROFESSOR.typeId()){ + } else if (instructorTypeId == VISITING_PROFESSOR.getId()){ currentTermSummary.put(VISITING_PROFESSOR_COUNT, currentTermSummary.get(VISITING_PROFESSOR_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(VISITING_PROFESSOR_COUNT, combinedTermSummary.get(VISITING_PROFESSOR_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(VISITING_PROFESSOR_COST, currentTermSummary.get(VISITING_PROFESSOR_COST).add(instructorCost)); combinedTermSummary.put(VISITING_PROFESSOR_COST, combinedTermSummary.get(VISITING_PROFESSOR_COST).add(instructorCost)); - } else if (instructorTypeId == ASSOCIATE_PROFESSOR.typeId()){ + } else if (instructorTypeId == ASSOCIATE_INSTRUCTOR.getId()){ currentTermSummary.put(ASSOCIATE_INSTRUCTOR_COUNT, currentTermSummary.get(ASSOCIATE_INSTRUCTOR_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(ASSOCIATE_INSTRUCTOR_COUNT, combinedTermSummary.get(ASSOCIATE_INSTRUCTOR_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(ASSOCIATE_INSTRUCTOR_COST, currentTermSummary.get(ASSOCIATE_INSTRUCTOR_COST).add(instructorCost)); combinedTermSummary.put(ASSOCIATE_INSTRUCTOR_COST, combinedTermSummary.get(ASSOCIATE_INSTRUCTOR_COST).add(instructorCost)); - } else if (instructorTypeId == UNIT18_LECTURER.typeId()){ + } else if (instructorTypeId == UNIT18_LECTURER.getId()){ currentTermSummary.put(UNIT18_LECTURER_COUNT, currentTermSummary.get(UNIT18_LECTURER_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(UNIT18_LECTURER_COUNT, combinedTermSummary.get(UNIT18_LECTURER_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(UNIT18_LECTURER_COST, currentTermSummary.get(UNIT18_LECTURER_COST).add(instructorCost)); combinedTermSummary.put(UNIT18_LECTURER_COST, combinedTermSummary.get(UNIT18_LECTURER_COST).add(instructorCost)); - } else if (instructorTypeId == CONTINUING_LECTURER.typeId()){ + } else if (instructorTypeId == CONTINUING_LECTURER.getId()){ currentTermSummary.put(CONTINUING_LECTURER_COUNT, currentTermSummary.get(CONTINUING_LECTURER_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(CONTINUING_LECTURER_COUNT, combinedTermSummary.get(CONTINUING_LECTURER_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(CONTINUING_LECTURER_COST, currentTermSummary.get(CONTINUING_LECTURER_COST).add(instructorCost)); combinedTermSummary.put(CONTINUING_LECTURER_COST, combinedTermSummary.get(CONTINUING_LECTURER_COST).add(instructorCost)); - } else if (instructorTypeId == CONTINUING_LECTURER_AUGMENTATION.typeId()){ + } else if (instructorTypeId == CONTINUING_LECTURER_AUGMENTATION.getId()){ currentTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COUNT, currentTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COUNT, combinedTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COST, currentTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COST).add(instructorCost)); combinedTermSummary.put(CONTINUING_LECTURER_AUGMENTATION_COST, combinedTermSummary.get(CONTINUING_LECTURER_AUGMENTATION_COST).add(instructorCost)); - } else if (instructorTypeId == LADDER_FACULTY.typeId()){ + } else if (instructorTypeId == LADDER_FACULTY.getId()){ currentTermSummary.put(LADDER_FACULTY_COUNT, currentTermSummary.get(LADDER_FACULTY_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(LADDER_FACULTY_COUNT, combinedTermSummary.get(LADDER_FACULTY_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(LADDER_FACULTY_COST, currentTermSummary.get(LADDER_FACULTY_COST).add(instructorCost)); combinedTermSummary.put(LADDER_FACULTY_COST, combinedTermSummary.get(LADDER_FACULTY_COST).add(instructorCost)); - } else if (instructorTypeId == INSTRUCTOR.typeId()){ + } else if (instructorTypeId == INSTRUCTOR.getId()){ currentTermSummary.put(INSTRUCTOR_COUNT, currentTermSummary.get(INSTRUCTOR_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(INSTRUCTOR_COUNT, combinedTermSummary.get(INSTRUCTOR_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(INSTRUCTOR_COST, currentTermSummary.get(INSTRUCTOR_COST).add(instructorCost)); combinedTermSummary.put(INSTRUCTOR_COST, combinedTermSummary.get(INSTRUCTOR_COST).add(instructorCost)); - } else if (instructorTypeId == LECTURER_SOE.typeId()){ + } else if (instructorTypeId == LECTURER_SOE.getId()){ currentTermSummary.put(LECTURER_SOE_COUNT, currentTermSummary.get(LECTURER_SOE_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(LECTURER_SOE_COUNT, combinedTermSummary.get(LECTURER_SOE_COUNT).add(BigDecimal.ONE)); currentTermSummary.put(LECTURER_SOE_COST, currentTermSummary.get(LECTURER_SOE_COST).add(instructorCost)); combinedTermSummary.put(LECTURER_SOE_COST, combinedTermSummary.get(LECTURER_SOE_COST).add(instructorCost)); - } else if (instructorTypeId == NEW_FACULTY_HIRE.typeId()){ + } else if (instructorTypeId == NEW_FACULTY_HIRE.getId()){ currentTermSummary.put(NEW_FACULTY_HIRE_COUNT, currentTermSummary.get(NEW_FACULTY_HIRE_COUNT).add(BigDecimal.ONE)); combinedTermSummary.put(NEW_FACULTY_HIRE_COUNT, combinedTermSummary.get(NEW_FACULTY_HIRE_COUNT).add(BigDecimal.ONE)); diff --git a/src/main/java/edu/ucdavis/dss/ipa/services/WorkloadAssignmentService.java b/src/main/java/edu/ucdavis/dss/ipa/services/WorkloadAssignmentService.java new file mode 100644 index 00000000..2c8ff889 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/services/WorkloadAssignmentService.java @@ -0,0 +1,15 @@ +package edu.ucdavis.dss.ipa.services; + +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; +import java.util.List; + +public interface WorkloadAssignmentService { + List findByWorkloadSnapshotId(long id); + + List generateWorkloadAssignments(long workgroupId, long year); + List generateWorkloadAssignments(long workgroupId, long year, WorkloadSnapshot workloadSnapshot); + List generateWorkloadAssignments(long workgroupId, long year, boolean includeUnassigned); + + List saveAll(List workloadAssignments); +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/services/WorkloadSnapshotService.java b/src/main/java/edu/ucdavis/dss/ipa/services/WorkloadSnapshotService.java new file mode 100644 index 00000000..012ca264 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/services/WorkloadSnapshotService.java @@ -0,0 +1,16 @@ +package edu.ucdavis.dss.ipa.services; + +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; +import java.util.List; + +public interface WorkloadSnapshotService { + + WorkloadSnapshot create(long workgroupId, long budgetScenarioId); + + WorkloadSnapshot findById(long workloadSnapshotId); + + List findByWorkgroupIdAndYear(long workgroupId, long year); + + void deleteByBudgetScenarioId(long budgetScenarioId); + +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/services/jpa/JpaWorkloadAssignmentService.java b/src/main/java/edu/ucdavis/dss/ipa/services/jpa/JpaWorkloadAssignmentService.java new file mode 100644 index 00000000..2f62ddc9 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/services/jpa/JpaWorkloadAssignmentService.java @@ -0,0 +1,524 @@ +package edu.ucdavis.dss.ipa.services.jpa; + +import edu.ucdavis.dss.dw.dto.DwCensus; +import edu.ucdavis.dss.ipa.entities.Course; +import edu.ucdavis.dss.ipa.entities.Instructor; +import edu.ucdavis.dss.ipa.entities.InstructorType; +import edu.ucdavis.dss.ipa.entities.Schedule; +import edu.ucdavis.dss.ipa.entities.ScheduleInstructorNote; +import edu.ucdavis.dss.ipa.entities.Section; +import edu.ucdavis.dss.ipa.entities.SectionGroup; +import edu.ucdavis.dss.ipa.entities.TeachingAssignment; +import edu.ucdavis.dss.ipa.entities.Term; +import edu.ucdavis.dss.ipa.entities.UserRole; +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; +import edu.ucdavis.dss.ipa.repositories.DataWarehouseRepository; +import edu.ucdavis.dss.ipa.repositories.WorkloadAssignmentRepository; +import edu.ucdavis.dss.ipa.services.CourseService; +import edu.ucdavis.dss.ipa.services.InstructorService; +import edu.ucdavis.dss.ipa.services.InstructorTypeService; +import edu.ucdavis.dss.ipa.services.ScheduleInstructorNoteService; +import edu.ucdavis.dss.ipa.services.ScheduleService; +import edu.ucdavis.dss.ipa.services.SectionGroupService; +import edu.ucdavis.dss.ipa.services.SectionService; +import edu.ucdavis.dss.ipa.services.TeachingAssignmentService; +import edu.ucdavis.dss.ipa.services.UserRoleService; +import edu.ucdavis.dss.ipa.services.WorkloadAssignmentService; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.springframework.stereotype.Service; + +@Service +public class JpaWorkloadAssignmentService implements WorkloadAssignmentService { + @Inject + WorkloadAssignmentRepository workloadAssignmentRepository; + @Inject + DataWarehouseRepository dwRepository; + @Inject + ScheduleService scheduleService; + @Inject + CourseService courseService; + @Inject + InstructorTypeService instructorTypeService; + @Inject + InstructorService instructorService; + @Inject + TeachingAssignmentService teachingAssignmentService; + @Inject + ScheduleInstructorNoteService scheduleInstructorNoteService; + @Inject + SectionGroupService sectionGroupService; + @Inject + SectionService sectionService; + @Inject + UserRoleService userRoleService; + + public List findByWorkloadSnapshotId(long workloadSnapshotId) { + return workloadAssignmentRepository.findByWorkloadSnapshotId(workloadSnapshotId); + } + + public List saveAll(List workloadAssignments) { + List results = new ArrayList<>(); + for (WorkloadAssignment workloadAssignment : workloadAssignments) { + results.add(workloadAssignmentRepository.save(workloadAssignment)); + } + return results; + } + + public List generateWorkloadAssignments(long workgroupId, long year) { + return generateWorkloadAssignments(workgroupId, year, false); + } + + public List generateWorkloadAssignments(long workgroupId, long year, boolean includeUnassigned) { + List workloadAssignments = new ArrayList<>(); + + // Gathering phase + Schedule schedule = scheduleService.findByWorkgroupIdAndYear(workgroupId, year); + List courses = schedule.getCourses(); + List sectionGroups = sectionGroupService.findByScheduleId(schedule.getId()); + List
sections = sectionService.findVisibleByWorkgroupIdAndYear(workgroupId, year); + List scheduleInstructorNotes = + scheduleInstructorNoteService.findByScheduleId(schedule.getId()); + List teachingAssignments = teachingAssignmentService.findApprovedByWorkgroupIdAndYear(workgroupId, year); + List instructorTypes = instructorTypeService.getAllInstructorTypes(); + + if (schedule == null) { + return null; + } + + Set instructorSet = new HashSet<>(); + Set activeInstructors = + new HashSet<>(instructorService.findActiveByWorkgroupId(schedule.getWorkgroup().getId())); + Set assignedInstructors = + new HashSet<>(instructorService.findAssignedByScheduleId(schedule.getId())); + + instructorSet.addAll(assignedInstructors); + instructorSet.addAll(activeInstructors); + + List instructors = new ArrayList<>(instructorSet); + List termCodes = new ArrayList<>(); + + // TODO: remove termCodes not in Schedule + termCodes.addAll(Term.getTermCodesByYear(year)); + termCodes.addAll(Term.getTermCodesByYear(year - 1)); + + // SubjectCode: [CourseNumbers] + Map> courseMap = new HashMap<>(); + + for (Course c : courses) { + if (!courseMap.containsKey(c.getSubjectCode())) { + courseMap.put(c.getSubjectCode(), new ArrayList<>()); + } + + courseMap.get(c.getSubjectCode()).add(c.getCourseNumber()); + } + + List>> termCodeCensusFutures = new ArrayList<>(); + List>> courseCensusFutures = new ArrayList<>(); + + for (String subjectCode : courseMap.keySet()) { + termCodeCensusFutures.addAll(termCodes.stream().map(termCode -> CompletableFuture.supplyAsync( + () -> dwRepository.getCensusBySubjectCodeAndTermCode(subjectCode, termCode))) + .collect(Collectors.toList())); + + courseCensusFutures.addAll(courseMap.get(subjectCode).stream().distinct().map( + courseNumber -> CompletableFuture.supplyAsync( + () -> dwRepository.getCensusBySubjectCodeAndCourseNumber(subjectCode, courseNumber))) + .collect(Collectors.toList())); + } + + List termCodeCensus = + termCodeCensusFutures.stream().map(CompletableFuture::join).flatMap(Collection::stream) + .filter(c -> "CURRENT".equals(c.getSnapshotCode())).collect(Collectors.toList()); + List courseCensus = + courseCensusFutures.stream().map(CompletableFuture::join).flatMap(Collection::stream) + .filter(c -> "CURRENT".equals(c.getSnapshotCode())).collect(Collectors.toList()); + + Map> termCodeCensusMap = generateCensusMap(termCodeCensus); + Map> courseCensusMap = generateCensusMap(courseCensus); + + courseCensus.stream().filter(c -> "009A".equals(c.getCourseNumber()) && "202301".equals(c.getTermCode())).collect(Collectors.toList()); + // Transform phase + String department = schedule.getWorkgroup().getName(); + + for (Instructor instructor : instructors) { + Long instructorTypeId = getInstructorTypeId(instructor, teachingAssignments, workgroupId); + + if (instructorTypeId == null) { + continue; + } + + String instructorTypeDescription = instructorTypeService.findById(instructorTypeId).getDescription(); + + List scheduleAssignments = teachingAssignments.stream().filter(ta -> ta.getInstructor() != null && ta.getInstructor().getId() == instructor.getId()).collect(Collectors.toList()); + + String instructorNote = scheduleInstructorNotes.stream() + .filter(note -> note.getInstructor().getId() == instructor.getId()) + .map(note -> note.getInstructorComment()).findAny().orElse(""); + + if (scheduleAssignments.size() == 0) { + WorkloadAssignment wa = new WorkloadAssignment(); + wa.setYear(year); + wa.setDepartment(department); + wa.setInstructorType(instructorTypeDescription); + wa.setName(instructor.getInvertedName()); + wa.setInstructorNote(instructorNote); + workloadAssignments.add(wa); + } else { + for (TeachingAssignment assignment : scheduleAssignments) { + String termCode = null, previousYearTermCode = null, courseDescription = null, offering = null, lastOfferedCensus = null, unit = null; + Integer plannedSeats = null; + Long censusCount = null; + Long previousYearCensus = null; + Float studentCreditHour = null; + + String courseType = getCourseType(assignment); + if (assignment.getSectionGroup() != null) { + SectionGroup sectionGroup = assignment.getSectionGroup(); + Course course = sectionGroup.getCourse(); + + termCode = sectionGroup.getTermCode(); + previousYearTermCode = Term.getPreviousYearTermCode(termCode); + courseDescription = course.getSubjectCode() + " " + course.getCourseNumber(); + offering = course.getSequencePattern(); + unit = sectionGroup.getDisplayUnits(); + plannedSeats = sectionGroup.getPlannedSeats(); + censusCount = 0L; + + if (termCodeCensus.size() > 0) { + String courseKey = course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + + course.getSequencePattern(); + + if (courseCensusMap.containsKey(termCode) && + courseCensusMap.get(termCode).containsKey(courseKey)) { + censusCount = courseCensusMap.get(termCode).get(courseKey); + + studentCreditHour = calculateStudentCreditHours(censusCount, course, sectionGroup); +// plannedSeats = sectionGroup.getPlannedSeats(); + } + + if (courseCensusMap.containsKey(previousYearTermCode) && + courseCensusMap.get(previousYearTermCode).containsKey(courseKey)) { + previousYearCensus = courseCensusMap.get(previousYearTermCode).get(courseKey); + } + + // find last offering term + List offeredTermCodes = + courseCensusMap.keySet().stream().sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + + String lastOfferedTermCode = null; + String lastOfferedCourseKey = + course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + + course.getSequencePattern(); + if (offeredTermCodes.size() > 2) { + for (String offeredTermCode : offeredTermCodes.subList(offeredTermCodes.indexOf(termCode) + 1, offeredTermCodes.size())) { + if (courseCensusMap.get(offeredTermCode).containsKey(lastOfferedCourseKey) && courseCensusMap.get(offeredTermCode).get(lastOfferedCourseKey) != 0) { + lastOfferedTermCode = offeredTermCode; + break; + } + } + if (lastOfferedTermCode != null) { + lastOfferedCensus = + courseCensusMap.get(lastOfferedTermCode).get(lastOfferedCourseKey) + " (" + + Term.getShortDescription(lastOfferedTermCode) + ")"; + } + } else { + lastOfferedCensus = ""; + } + } + } else { + termCode = assignment.getTermCode(); + courseDescription = assignment.getDescription(); + } + + WorkloadAssignment wa = new WorkloadAssignment(); + wa.setYear(year); + wa.setDepartment(department); + wa.setInstructorType(instructorTypeDescription); + wa.setName(instructor.getInvertedName()); + wa.setTermCode(termCode); + wa.setCourseType(courseType); + wa.setDescription(courseDescription); + wa.setOffering(offering); + wa.setCensus(censusCount); + wa.setPlannedSeats(plannedSeats); + wa.setPreviousYearCensus(previousYearCensus); + wa.setLastOfferedCensus(lastOfferedCensus); + wa.setUnits(unit); + wa.setStudentCreditHours(studentCreditHour); + wa.setInstructorNote(instructorNote); + + workloadAssignments.add(wa); + } + } + } + + // fill in TBD instructor assignments + List unnamedAssignments = + teachingAssignments.stream().filter(teachingAssignment -> teachingAssignment.getInstructor() == null) + .collect(Collectors.toList()); + for (TeachingAssignment teachingAssignment : unnamedAssignments) { + SectionGroup sectionGroup = teachingAssignment.getSectionGroup(); + Course course = sectionGroup.getCourse(); + String termCode = sectionGroup.getTermCode(); + long censusCount = 0; + long previousYearCensus = 0; + Float studentCreditHour = null; + String previousYearTermCode = + Integer.parseInt(termCode.substring(0, 4)) - 1 + termCode.substring(4, 6); + String lastOfferedCensus = null; + + if (termCodeCensus.size() > 0) { + String courseKey = course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + + course.getSequencePattern(); + + if (courseCensusMap.containsKey(termCode) && + courseCensusMap.get(termCode).containsKey(courseKey)) { + censusCount = courseCensusMap.get(termCode).get(courseKey); + + studentCreditHour = calculateStudentCreditHours(censusCount, course, sectionGroup); +// plannedSeats = sectionGroup.getPlannedSeats(); + } + + if (courseCensusMap.containsKey(previousYearTermCode) && + courseCensusMap.get(previousYearTermCode).containsKey(courseKey)) { + previousYearCensus = courseCensusMap.get(previousYearTermCode).get(courseKey); + } + + // find last offering term + List offeredTermCodes = + courseCensusMap.keySet().stream().sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + + String lastOfferedTermCode = null; + String lastOfferedCourseKey = + course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + + course.getSequencePattern(); + if (offeredTermCodes.size() > 2) { + for (String offeredTermCode : offeredTermCodes.subList(offeredTermCodes.indexOf(termCode) + 1, offeredTermCodes.size())) { + if (courseCensusMap.get(offeredTermCode).containsKey(lastOfferedCourseKey) && courseCensusMap.get(offeredTermCode).get(lastOfferedCourseKey) != 0) { + lastOfferedTermCode = offeredTermCode; + break; + } + } + + if (lastOfferedTermCode != null) { + lastOfferedCensus = + courseCensusMap.get(lastOfferedTermCode).get(lastOfferedCourseKey) + " (" + + Term.getShortDescription(lastOfferedTermCode) + ")"; + } + } else { + lastOfferedCensus = ""; + } + } + + WorkloadAssignment wa = new WorkloadAssignment(); + wa.setYear(year); + wa.setDepartment(department); + wa.setInstructorType( + instructorTypeService.findById(teachingAssignment.getInstructorTypeIdentification()).getDescription()); + wa.setName("TBD"); + wa.setTermCode(sectionGroup.getTermCode()); + wa.setCourseType(getCourseType(teachingAssignment)); + wa.setDescription(course.getSubjectCode() + " " + course.getCourseNumber()); + wa.setOffering(course.getSequencePattern()); + wa.setCensus(censusCount); + wa.setPlannedSeats(sectionGroup.getPlannedSeats()); + wa.setPreviousYearCensus(previousYearCensus); + wa.setLastOfferedCensus(lastOfferedCensus); + wa.setUnits(sectionGroup.getDisplayUnits()); + workloadAssignments.add(wa); + } + + // fill in unassigned data for snapshots + if (includeUnassigned) { + List unassignedSectionGroups = sectionGroups.stream().filter(sg -> + sg.getTeachingAssignments().stream().filter(ta -> ta.isApproved()).collect(Collectors.toList()) + .size() == 0).collect(Collectors.toList()); + + for (SectionGroup sectionGroup : unassignedSectionGroups) { + Course course = sectionGroup.getCourse(); + String termCode = sectionGroup.getTermCode(); + long censusCount = 0; + long previousYearCensus = 0; + Float studentCreditHour = null; + String previousYearTermCode = + Integer.parseInt(termCode.substring(0, 4)) - 1 + termCode.substring(4, 6); + String lastOfferedCensus = null; + + if (termCodeCensus.size() > 0) { + String courseKey = course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + + course.getSequencePattern(); + + if (courseCensusMap.containsKey(termCode) && + courseCensusMap.get(termCode).containsKey(courseKey)) { + censusCount = courseCensusMap.get(termCode).get(courseKey); + + studentCreditHour = calculateStudentCreditHours(censusCount, course, sectionGroup); +// plannedSeats = sectionGroup.getPlannedSeats(); + } + + if (courseCensusMap.containsKey(previousYearTermCode) && + courseCensusMap.get(previousYearTermCode).containsKey(courseKey)) { + previousYearCensus = courseCensusMap.get(previousYearTermCode).get(courseKey); + } + + // find last offering term + List offeredTermCodes = + courseCensusMap.keySet().stream().sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + + String lastOfferedTermCode = null; + String lastOfferedCourseKey = + course.getSubjectCode() + "-" + course.getCourseNumber() + "-" + + course.getSequencePattern(); + if (offeredTermCodes.size() > 2) { + // walk through map to check for course key + for (String offeredTermCode : offeredTermCodes.subList(offeredTermCodes.indexOf(termCode) + 1, + offeredTermCodes.size())) { + if (courseCensusMap.get(offeredTermCode).containsKey(lastOfferedCourseKey) && + courseCensusMap.get(offeredTermCode).get(lastOfferedCourseKey) != 0) { + lastOfferedTermCode = offeredTermCode; + break; + } + } +// lastOfferedTermCode = offeredTermCodes.get(offeredTermCodes.size() - 2); + + if (lastOfferedTermCode != null) { + lastOfferedCensus = + courseCensusMap.get(lastOfferedTermCode).get(lastOfferedCourseKey) + " (" + + Term.getShortDescription(lastOfferedTermCode) + ")"; + } + } else { + lastOfferedCensus = ""; + } + } + + WorkloadAssignment wa = new WorkloadAssignment(); + wa.setYear(year); + wa.setDepartment(department); + wa.setInstructorType("Unassigned"); + wa.setName(""); + wa.setTermCode(sectionGroup.getTermCode()); + wa.setCourseType(getCourseType(sectionGroup)); + wa.setDescription(course.getSubjectCode() + " " + course.getCourseNumber()); + wa.setOffering(course.getSequencePattern()); + wa.setCensus(censusCount); + wa.setPlannedSeats(sectionGroup.getPlannedSeats()); + wa.setStudentCreditHours(studentCreditHour); + wa.setPreviousYearCensus(previousYearCensus); + wa.setLastOfferedCensus(lastOfferedCensus); + wa.setUnits(sectionGroup.getDisplayUnits()); + workloadAssignments.add(wa); + } + } + + return workloadAssignments; + } + + public List generateWorkloadAssignments(long workgroupId, long year, WorkloadSnapshot workloadSnapshot) { + List assignments = generateWorkloadAssignments(workgroupId, year, true); + + for (WorkloadAssignment a : assignments) { + a.setWorkloadSnapshot(workloadSnapshot); + } + + return assignments; + } + + private String calculateCourseType(Course course) { + int courseNumbers = Integer.parseInt(course.getCourseNumber().replaceAll("[^\\d.]", "")); + + if (courseNumbers < 100) { + return "Lower"; + } else if (courseNumbers >= 200) { + return "Grad"; + } else { + return "Upper"; + } + } + + private String getCourseType(TeachingAssignment teachingAssignment) { + if (teachingAssignment.getSectionGroup() == null) { + // non-Course Assignment + return teachingAssignment.getDescription(); + } else { + return calculateCourseType(teachingAssignment.getSectionGroup().getCourse()); + } + } + + private String getCourseType(SectionGroup sectionGroup) { + return calculateCourseType(sectionGroup.getCourse()); + } + + private Long getInstructorTypeId(Instructor instructor, List teachingAssignments, + long workgroupId) { + // attempt by userRole + UserRole userRole = + userRoleService.findOrCreateByLoginIdAndWorkgroupIdAndRoleToken(instructor.getLoginId(), workgroupId, + "instructor"); + + if (userRole != null) { + return userRole.getInstructorTypeIdentification(); + } + // attempt by teachingAssignment + TeachingAssignment teachingAssignment = + teachingAssignments.stream().filter(ta -> ta.getInstructor().getId() == instructor.getId()).findFirst() + .get(); + + if (teachingAssignment != null) { + return teachingAssignment.getInstructorType().getId(); + } + + return null; + } + + private Float calculateStudentCreditHours(Long students, Course course, SectionGroup sectionGroup) { + Float units = 0.0f; + + if (sectionGroup.getUnitsVariable() != null) { + units = sectionGroup.getUnitsVariable(); + } else if (course.getUnitsLow() != null && course.getUnitsLow() > 0) { + units = course.getUnitsLow(); + } + + return students * units; + } + + /** + * @param censuses + * @return { "termCode": { "course": enrollmentCount } } + */ + private Map> generateCensusMap(List censuses) { + Map> censusMap = new HashMap<>(); + for (DwCensus census : censuses) { + String termCode = census.getTermCode(); + String courseKey = + census.getSubjectCode() + "-" + census.getCourseNumber() + "-" + census.getSequencePattern(); + + if (censusMap.get(termCode) == null) { + censusMap.put(termCode, new HashMap<>()); + } + + if (censusMap.get(termCode).get(courseKey) == null) { + censusMap.get(termCode).put(courseKey, census.getCurrentEnrolledCount()); + } else { + censusMap.get(termCode) + .put(courseKey, censusMap.get(termCode).get(courseKey) + census.getCurrentEnrolledCount()); + } + } + + return censusMap; + } +} diff --git a/src/main/java/edu/ucdavis/dss/ipa/services/jpa/JpaWorkloadSnapshotService.java b/src/main/java/edu/ucdavis/dss/ipa/services/jpa/JpaWorkloadSnapshotService.java new file mode 100644 index 00000000..4ff9a462 --- /dev/null +++ b/src/main/java/edu/ucdavis/dss/ipa/services/jpa/JpaWorkloadSnapshotService.java @@ -0,0 +1,59 @@ +package edu.ucdavis.dss.ipa.services.jpa; + +import edu.ucdavis.dss.ipa.entities.BudgetScenario; +import edu.ucdavis.dss.ipa.entities.Workgroup; +import edu.ucdavis.dss.ipa.entities.WorkloadAssignment; +import edu.ucdavis.dss.ipa.entities.WorkloadSnapshot; +import edu.ucdavis.dss.ipa.repositories.WorkloadSnapshotRepository; +import edu.ucdavis.dss.ipa.services.BudgetScenarioService; +import edu.ucdavis.dss.ipa.services.WorkgroupService; +import edu.ucdavis.dss.ipa.services.WorkloadAssignmentService; +import edu.ucdavis.dss.ipa.services.WorkloadSnapshotService; +import java.util.List; +import javax.inject.Inject; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class JpaWorkloadSnapshotService implements WorkloadSnapshotService { + @Inject WorkloadSnapshotRepository workloadSnapshotRepository; + @Inject + BudgetScenarioService budgetScenarioService; + @Inject WorkgroupService workgroupService; + @Inject WorkloadAssignmentService workloadAssignmentService; + + public WorkloadSnapshot findById(long workloadSnapshotId) { + return workloadSnapshotRepository.findById(workloadSnapshotId); + } + + public List findByWorkgroupIdAndYear(long workgroupId, long year) { + return workloadSnapshotRepository.findByWorkgroupIdAndYear(workgroupId, year); + } + + @Transactional + public WorkloadSnapshot create(long workgroupId, long budgetScenarioId) { + Workgroup workgroup = workgroupService.findOneById(workgroupId); + BudgetScenario budgetScenario = budgetScenarioService.findById(budgetScenarioId); + long year = budgetScenario.getBudget().getSchedule().getYear(); + + WorkloadSnapshot snapshot = new WorkloadSnapshot(); + + snapshot.setName(budgetScenario.getName()); + snapshot.setBudgetScenario(budgetScenario); + snapshot.setWorkgroup(workgroup); + snapshot.setYear(year); + snapshot = workloadSnapshotRepository.save(snapshot); + List workloadAssignments = workloadAssignmentService.generateWorkloadAssignments(workgroupId, year, snapshot); + snapshot.setWorkloadAssignments(workloadAssignments); + + return workloadSnapshotRepository.save(snapshot); + } + + public void deleteByBudgetScenarioId(long budgetScenarioId) { + WorkloadSnapshot snapshot = workloadSnapshotRepository.findByBudgetScenarioId(budgetScenarioId); + + if (snapshot != null) { + workloadSnapshotRepository.delete(snapshot); + } + }; +} diff --git a/src/main/resources/db/migration/V254__Add_Workload_Snapshot_Table.sql b/src/main/resources/db/migration/V254__Add_Workload_Snapshot_Table.sql new file mode 100644 index 00000000..b694f825 --- /dev/null +++ b/src/main/resources/db/migration/V254__Add_Workload_Snapshot_Table.sql @@ -0,0 +1,35 @@ +CREATE TABLE `WorkloadSnapshots` +( + `Id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `Name` VARCHAR(255) NOT NULL, + `BudgetScenarioId` INT NOT NULL, + `WorkgroupId` INT NOT NULL, + `Year` INT NOT NULL, + `CreatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `UpdatedAt` timestamp NULL DEFAULT NULL, + `ModifiedBy` varchar(16) DEFAULT NULL +); + +CREATE TABLE `WorkloadAssignments` +( + `Id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `WorkloadSnapshotId` INT NOT NULL, + `Year` INT NULL DEFAULT NULL, + `Department` VARCHAR(100) NULL DEFAULT NULL, + `InstructorType` VARCHAR(100) NULL DEFAULT NULL, + `Name` VARCHAR(100) NULL DEFAULT NULL, + `TermCode` VARCHAR(6) NULL DEFAULT NULL, + `CourseType` VARCHAR(100) NULL DEFAULT NULL, + `Description` VARCHAR(100) NULL DEFAULT NULL, + `Offering` VARCHAR(100) NULL DEFAULT NULL, + `LastOfferedCensus` VARCHAR(100) NULL DEFAULT NULL, + `Units` VARCHAR(100) NULL DEFAULT NULL, + `InstructorNote` TEXT NULL DEFAULT NULL, + `Census` VARCHAR(100) NULL DEFAULT NULL, + `PreviousYearCensus` INT NULL DEFAULT NULL, + `PlannedSeats` INT NULL DEFAULT NULL, + `StudentCreditHours` FLOAT NULL DEFAULT NULL, + `CreatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `UpdatedAt` timestamp NULL DEFAULT NULL, + `ModifiedBy` varchar(16) DEFAULT NULL +);