| 1 | package edu.ucsb.cs156.happiercows.controllers; | |
| 2 | ||
| 3 | import edu.ucsb.cs156.happiercows.entities.Staff; | |
| 4 | import edu.ucsb.cs156.happiercows.errors.EntityNotFoundException; | |
| 5 | import edu.ucsb.cs156.happiercows.models.CsvUploadResult; | |
| 6 | import edu.ucsb.cs156.happiercows.models.StaffDTO; | |
| 7 | import edu.ucsb.cs156.happiercows.repositories.StaffRepository; | |
| 8 | import io.swagger.v3.oas.annotations.tags.Tag; | |
| 9 | import io.swagger.v3.oas.annotations.Operation; | |
| 10 | import io.swagger.v3.oas.annotations.Parameter; | |
| 11 | import lombok.extern.slf4j.Slf4j; | |
| 12 | import org.apache.commons.csv.CSVFormat; | |
| 13 | import org.apache.commons.csv.CSVParser; | |
| 14 | import org.apache.commons.csv.CSVRecord; | |
| 15 | import org.springframework.beans.factory.annotation.Autowired; | |
| 16 | import org.springframework.http.MediaType; | |
| 17 | import org.springframework.security.access.prepost.PreAuthorize; | |
| 18 | import org.springframework.web.bind.annotation.*; | |
| 19 | import org.springframework.web.multipart.MultipartFile; | |
| 20 | ||
| 21 | import java.io.IOException; | |
| 22 | import java.io.InputStreamReader; | |
| 23 | import java.nio.charset.StandardCharsets; | |
| 24 | import java.util.ArrayList; | |
| 25 | import java.util.Iterator; | |
| 26 | import java.util.List; | |
| 27 | ||
| 28 | @Slf4j | |
| 29 | @Tag(name = "Staff") | |
| 30 | @RequestMapping("/api/staff") | |
| 31 | @RestController | |
| 32 | public class StaffController extends ApiController { | |
| 33 | @Autowired | |
| 34 | private StaffRepository staffRepository; | |
| 35 | ||
| 36 | @Operation(summary = "List all course staff") | |
| 37 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 38 | @GetMapping("/all") | |
| 39 | public Iterable<Staff> allStaff() { | |
| 40 |
1
1. allStaff : replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StaffController::allStaff → KILLED |
return staffRepository.findAll(); |
| 41 | } | |
| 42 | ||
| 43 | @Operation(summary = "List the staff for a single course") | |
| 44 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 45 | @GetMapping("/course/{courseId}") | |
| 46 | public Iterable<Staff> staffForCourse( | |
| 47 | @Parameter(name = "courseId") @PathVariable Long courseId) { | |
| 48 |
1
1. staffForCourse : replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StaffController::staffForCourse → KILLED |
return staffRepository.findByCourseId(courseId); |
| 49 | } | |
| 50 | ||
| 51 | @Operation(summary = "Get a staff member by id") | |
| 52 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 53 | @GetMapping("/{id}") | |
| 54 | public Staff getStaffById( | |
| 55 | @Parameter(name = "id") @PathVariable Long id) { | |
| 56 |
1
1. getStaffById : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::getStaffById → KILLED |
return staffRepository.findById(id) |
| 57 |
1
1. lambda$getStaffById$0 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$getStaffById$0 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Staff.class, id)); |
| 58 | } | |
| 59 | ||
| 60 | @Operation(summary = "Add a staff member to a course") | |
| 61 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 62 | @PostMapping("") | |
| 63 | public Staff postStaff( | |
| 64 | @Parameter(name = "staff") @RequestBody StaffDTO staffDTO) { | |
| 65 |
1
1. postStaff : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::postStaff → KILLED |
return staffRepository.save(staffDTO.toStaff()); |
| 66 | } | |
| 67 | ||
| 68 | @Operation(summary = "Update a single staff member") | |
| 69 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 70 | @PutMapping("/{id}") | |
| 71 | public Staff updateStaff( | |
| 72 | @Parameter(name = "id") @PathVariable Long id, @RequestBody StaffDTO staffDTO) { | |
| 73 | Staff staff = staffRepository.findById(id) | |
| 74 |
1
1. lambda$updateStaff$1 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$updateStaff$1 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Staff.class, id)); |
| 75 | ||
| 76 |
1
1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setLastName → KILLED |
staff.setLastName(staffDTO.getLastName()); |
| 77 |
1
1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setFirstMiddleName → KILLED |
staff.setFirstMiddleName(staffDTO.getFirstMiddleName()); |
| 78 |
1
1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setEmail → KILLED |
staff.setEmail(staffDTO.getEmail()); |
| 79 |
1
1. updateStaff : removed call to edu/ucsb/cs156/happiercows/entities/Staff::setCourseId → KILLED |
staff.setCourseId(staffDTO.getCourseId()); |
| 80 | ||
| 81 | staffRepository.save(staff); | |
| 82 | ||
| 83 |
1
1. updateStaff : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::updateStaff → KILLED |
return staff; |
| 84 | } | |
| 85 | ||
| 86 | @Operation(summary = "Remove a staff member from a course") | |
| 87 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 88 | @DeleteMapping("/{id}") | |
| 89 | public Object deleteStaff( | |
| 90 | @Parameter(name = "id") @PathVariable Long id) { | |
| 91 | Staff staff = staffRepository.findById(id) | |
| 92 |
1
1. lambda$deleteStaff$2 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::lambda$deleteStaff$2 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Staff.class, id)); |
| 93 | ||
| 94 |
1
1. deleteStaff : removed call to edu/ucsb/cs156/happiercows/repositories/StaffRepository::delete → KILLED |
staffRepository.delete(staff); |
| 95 |
1
1. deleteStaff : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::deleteStaff → KILLED |
return genericMessage("Staff with id %s deleted".formatted(id)); |
| 96 | } | |
| 97 | ||
| 98 | private static final List<String> STAFF_CSV_HEADERS = List.of("lastName", "firstMiddleName", "email"); | |
| 99 | ||
| 100 | /** | |
| 101 | * Bulk-adds staff to a course from an uploaded CSV file, with a single | |
| 102 | * fixed header format ({@code lastName,firstMiddleName,email}). Rows | |
| 103 | * whose email is already on the course's staff list are skipped rather | |
| 104 | * than duplicated. | |
| 105 | * | |
| 106 | * @param courseId the course to add the staff to | |
| 107 | * @param file the uploaded CSV file | |
| 108 | * @return a summary of how many staff were created, and which emails | |
| 109 | * were skipped as already-on-roster | |
| 110 | */ | |
| 111 | @Operation(summary = "Upload a CSV file of staff for a course") | |
| 112 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 113 | @PostMapping(value = "/upload/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | |
| 114 | public CsvUploadResult uploadStaffCsv( | |
| 115 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 116 | @Parameter(name = "file") @RequestParam("file") MultipartFile file) throws IOException { | |
| 117 | ||
| 118 | List<CSVRecord> records; | |
| 119 | try (CSVParser parser = CSVFormat.DEFAULT.parse( | |
| 120 | new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { | |
| 121 | records = parser.getRecords(); | |
| 122 | } | |
| 123 | ||
| 124 |
1
1. uploadStaffCsv : negated conditional → KILLED |
if (records.isEmpty()) { |
| 125 | throw new IllegalArgumentException("CSV file is empty"); | |
| 126 | } | |
| 127 | ||
| 128 | Iterator<CSVRecord> rows = records.iterator(); | |
| 129 | ||
| 130 | List<String> headerValues = new ArrayList<>(); | |
| 131 | for (String value : rows.next()) { | |
| 132 | headerValues.add(value.trim()); | |
| 133 | } | |
| 134 | ||
| 135 |
1
1. uploadStaffCsv : negated conditional → KILLED |
if (headerValues.size() != STAFF_CSV_HEADERS.size()) { |
| 136 | throw new IllegalArgumentException("Unrecognized CSV header format"); | |
| 137 | } | |
| 138 |
2
1. uploadStaffCsv : negated conditional → KILLED 2. uploadStaffCsv : changed conditional boundary → KILLED |
for (int i = 0; i < STAFF_CSV_HEADERS.size(); i++) { |
| 139 |
1
1. uploadStaffCsv : negated conditional → KILLED |
if (!STAFF_CSV_HEADERS.get(i).equalsIgnoreCase(headerValues.get(i))) { |
| 140 | throw new IllegalArgumentException("Unrecognized CSV header format"); | |
| 141 | } | |
| 142 | } | |
| 143 | ||
| 144 | int created = 0; | |
| 145 | List<String> skippedEmails = new ArrayList<>(); | |
| 146 | ||
| 147 |
1
1. uploadStaffCsv : negated conditional → KILLED |
while (rows.hasNext()) { |
| 148 | CSVRecord row = rows.next(); | |
| 149 |
1
1. uploadStaffCsv : negated conditional → KILLED |
if (row.size() != STAFF_CSV_HEADERS.size()) { |
| 150 | throw new IllegalArgumentException( | |
| 151 | "Row %d does not have the expected number of columns".formatted(row.getRecordNumber())); | |
| 152 | } | |
| 153 | ||
| 154 | Staff staff = Staff.builder() | |
| 155 | .lastName(row.get(0).trim()) | |
| 156 | .firstMiddleName(row.get(1).trim()) | |
| 157 | .email(row.get(2).trim()) | |
| 158 | .courseId(courseId) | |
| 159 | .build(); | |
| 160 | ||
| 161 |
1
1. uploadStaffCsv : negated conditional → KILLED |
if (staffRepository.findByCourseIdAndEmail(courseId, staff.getEmail()).iterator().hasNext()) { |
| 162 | skippedEmails.add(staff.getEmail()); | |
| 163 | continue; | |
| 164 | } | |
| 165 | ||
| 166 | staffRepository.save(staff); | |
| 167 |
1
1. uploadStaffCsv : Changed increment from 1 to -1 → KILLED |
created++; |
| 168 | } | |
| 169 | ||
| 170 |
1
1. uploadStaffCsv : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StaffController::uploadStaffCsv → KILLED |
return CsvUploadResult.builder() |
| 171 | .created(created) | |
| 172 | .skippedEmails(skippedEmails) | |
| 173 | .build(); | |
| 174 | } | |
| 175 | } | |
Mutations | ||
| 40 |
1.1 |
|
| 48 |
1.1 |
|
| 56 |
1.1 |
|
| 57 |
1.1 |
|
| 65 |
1.1 |
|
| 74 |
1.1 |
|
| 76 |
1.1 |
|
| 77 |
1.1 |
|
| 78 |
1.1 |
|
| 79 |
1.1 |
|
| 83 |
1.1 |
|
| 92 |
1.1 |
|
| 94 |
1.1 |
|
| 95 |
1.1 |
|
| 124 |
1.1 |
|
| 135 |
1.1 |
|
| 138 |
1.1 2.2 |
|
| 139 |
1.1 |
|
| 147 |
1.1 |
|
| 149 |
1.1 |
|
| 161 |
1.1 |
|
| 167 |
1.1 |
|
| 170 |
1.1 |