| 1 | package edu.ucsb.cs.scaffold.controller; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.core.JsonProcessingException; | |
| 4 | import com.opencsv.CSVReader; | |
| 5 | import com.opencsv.exceptions.CsvException; | |
| 6 | import edu.ucsb.cs.scaffold.entity.Course; | |
| 7 | import edu.ucsb.cs.scaffold.entity.RosterStudent; | |
| 8 | import edu.ucsb.cs.scaffold.enums.InsertStatus; | |
| 9 | import edu.ucsb.cs.scaffold.enums.RosterStatus; | |
| 10 | import edu.ucsb.cs.scaffold.errors.EntityNotFoundException; | |
| 11 | import edu.ucsb.cs.scaffold.model.LoadResult; | |
| 12 | import edu.ucsb.cs.scaffold.model.UpsertResponse; | |
| 13 | import edu.ucsb.cs.scaffold.repository.CourseRepository; | |
| 14 | import edu.ucsb.cs.scaffold.repository.RosterStudentRepository; | |
| 15 | import edu.ucsb.cs.scaffold.services.UpdateUserService; | |
| 16 | import edu.ucsb.cs156.jobs.services.JobService; | |
| 17 | import io.swagger.v3.oas.annotations.Operation; | |
| 18 | import io.swagger.v3.oas.annotations.Parameter; | |
| 19 | import io.swagger.v3.oas.annotations.tags.Tag; | |
| 20 | import java.io.BufferedInputStream; | |
| 21 | import java.io.IOException; | |
| 22 | import java.io.InputStream; | |
| 23 | import java.io.InputStreamReader; | |
| 24 | import java.util.ArrayList; | |
| 25 | import java.util.Arrays; | |
| 26 | import java.util.HashMap; | |
| 27 | import java.util.List; | |
| 28 | import java.util.Map; | |
| 29 | import lombok.extern.slf4j.Slf4j; | |
| 30 | import org.springframework.beans.factory.annotation.Autowired; | |
| 31 | import org.springframework.http.HttpStatus; | |
| 32 | import org.springframework.http.ResponseEntity; | |
| 33 | import org.springframework.security.access.prepost.PreAuthorize; | |
| 34 | import org.springframework.web.bind.annotation.PostMapping; | |
| 35 | import org.springframework.web.bind.annotation.RequestMapping; | |
| 36 | import org.springframework.web.bind.annotation.RequestParam; | |
| 37 | import org.springframework.web.bind.annotation.RestController; | |
| 38 | import org.springframework.web.multipart.MultipartFile; | |
| 39 | import org.springframework.web.server.ResponseStatusException; | |
| 40 | ||
| 41 | @Tag(name = "RosterStudents") | |
| 42 | @RequestMapping("/api/rosterstudents") | |
| 43 | @RestController | |
| 44 | @Slf4j | |
| 45 | public class RosterStudentsCSVController extends ApiController { | |
| 46 | ||
| 47 | @Autowired private RosterStudentRepository rosterStudentRepository; | |
| 48 | ||
| 49 | @Autowired private CourseRepository courseRepository; | |
| 50 | ||
| 51 | @Autowired private UpdateUserService updateUserService; | |
| 52 | @Autowired private JobService jobService; | |
| 53 | ||
| 54 | public enum RosterSourceType { | |
| 55 | UCSB_EGRADES, | |
| 56 | CHICO_CANVAS, | |
| 57 | OREGON_STATE, | |
| 58 | ROSTER_DOWNLOAD, | |
| 59 | UNKNOWN | |
| 60 | } | |
| 61 | ||
| 62 | public static final String UCSB_EGRADES_HEADERS = | |
| 63 | "Enrl Cd,Perm #,Grade,Final Units,Student Last,Student First Middle,Quarter,Course ID,Section,Meeting Time(s) / Location(s),Email,ClassLevel,Major1,Major2,Date/Time,Pronoun"; | |
| 64 | public static final String CHICO_CANVAS_HEADERS = | |
| 65 | "Student Name,Student ID,Student SIS ID,Email,Section Name"; | |
| 66 | public static final String OREGON_STATE_HEADERS = | |
| 67 | "Full name,Sortable name,Canvas user id,Overall course grade,Assignment on time percent,Last page view time,Last participation time,Last logged out,Email,SIS Id"; | |
| 68 | public static final String ROSTER_DOWNLOAD_HEADERS = | |
| 69 | "COURSEID,EMAIL,FIRSTNAME,GITHUBID,GITHUBLOGIN,ID,LASTNAME,ORGSTATUS,ROSTERSTATUS,SECTION,STUDENTID,TEAMS,USERID"; | |
| 70 | ||
| 71 | public static RosterSourceType getRosterSourceType(String[] headers) { | |
| 72 | ||
| 73 | Map<RosterSourceType, String[]> sourceTypeToHeaders = new HashMap<>(); | |
| 74 | ||
| 75 | sourceTypeToHeaders.put(RosterSourceType.UCSB_EGRADES, UCSB_EGRADES_HEADERS.split(",")); | |
| 76 | sourceTypeToHeaders.put(RosterSourceType.CHICO_CANVAS, CHICO_CANVAS_HEADERS.split(",")); | |
| 77 | sourceTypeToHeaders.put(RosterSourceType.OREGON_STATE, OREGON_STATE_HEADERS.split(",")); | |
| 78 | sourceTypeToHeaders.put(RosterSourceType.ROSTER_DOWNLOAD, ROSTER_DOWNLOAD_HEADERS.split(",")); | |
| 79 | ||
| 80 | for (Map.Entry<RosterSourceType, String[]> entry : sourceTypeToHeaders.entrySet()) { | |
| 81 | RosterSourceType type = entry.getKey(); | |
| 82 | String[] expectedHeaders = entry.getValue(); | |
| 83 |
2
1. getRosterSourceType : changed conditional boundary → KILLED 2. getRosterSourceType : negated conditional → KILLED |
if (headers.length >= expectedHeaders.length) { |
| 84 | boolean matches = true; | |
| 85 |
2
1. getRosterSourceType : negated conditional → KILLED 2. getRosterSourceType : changed conditional boundary → KILLED |
for (int i = 0; i < expectedHeaders.length; i++) { |
| 86 |
1
1. getRosterSourceType : negated conditional → KILLED |
if (!expectedHeaders[i].trim().equalsIgnoreCase(headers[i].trim())) { |
| 87 | matches = false; | |
| 88 | break; | |
| 89 | } | |
| 90 | } | |
| 91 |
1
1. getRosterSourceType : negated conditional → KILLED |
if (matches) { |
| 92 |
1
1. getRosterSourceType : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::getRosterSourceType → KILLED |
return type; |
| 93 | } | |
| 94 | } | |
| 95 | } | |
| 96 | // If no known type matches, return UNKNOWN | |
| 97 |
1
1. getRosterSourceType : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::getRosterSourceType → KILLED |
return RosterSourceType.UNKNOWN; |
| 98 | } | |
| 99 | ||
| 100 | /** | |
| 101 | * Upload Roster students for Course in any supported format. It is important to keep the code in | |
| 102 | * this method consistent with the code for adding a single roster student | |
| 103 | * | |
| 104 | * @param courseId | |
| 105 | * @param file | |
| 106 | * @return | |
| 107 | * @throws JsonProcessingException | |
| 108 | * @throws IOException | |
| 109 | * @throws CsvException | |
| 110 | */ | |
| 111 | @Operation(summary = "Upload Roster students for Course in any supported Format") | |
| 112 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 113 | @PostMapping( | |
| 114 | value = "/upload/csv", | |
| 115 | consumes = {"multipart/form-data"}) | |
| 116 | public ResponseEntity<LoadResult> uploadRosterStudentsCSV( | |
| 117 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 118 | @Parameter(name = "file") @RequestParam("file") MultipartFile file) | |
| 119 | throws JsonProcessingException, IOException, CsvException { | |
| 120 | ||
| 121 | Course course = | |
| 122 | courseRepository | |
| 123 | .findById(courseId) | |
| 124 |
1
1. lambda$uploadRosterStudentsCSV$0 : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::lambda$uploadRosterStudentsCSV$0 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId.toString())); |
| 125 | ||
| 126 | course.getRosterStudents().stream() | |
| 127 |
2
1. lambda$uploadRosterStudentsCSV$1 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::lambda$uploadRosterStudentsCSV$1 → KILLED 2. lambda$uploadRosterStudentsCSV$1 : negated conditional → KILLED |
.filter(filteredStudent -> filteredStudent.getRosterStatus() == RosterStatus.ROSTER) |
| 128 |
2
1. lambda$uploadRosterStudentsCSV$2 : removed call to edu/ucsb/cs/scaffold/entity/RosterStudent::setRosterStatus → KILLED 2. uploadRosterStudentsCSV : removed call to java/util/stream/Stream::forEach → KILLED |
.forEach(student -> student.setRosterStatus(RosterStatus.DROPPED)); |
| 129 | ||
| 130 | int counts[] = {0, 0}; | |
| 131 | List<RosterStudent> rejectedStudents = new ArrayList<>(); | |
| 132 | ||
| 133 | try (InputStream inputStream = new BufferedInputStream(file.getInputStream()); | |
| 134 | InputStreamReader reader = new InputStreamReader(inputStream); | |
| 135 | CSVReader csvReader = new CSVReader(reader); ) { | |
| 136 | ||
| 137 | String[] headers = csvReader.readNext(); | |
| 138 | RosterSourceType sourceType = getRosterSourceType(headers); | |
| 139 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (sourceType == RosterSourceType.UNKNOWN) { |
| 140 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown Roster Source Type"); | |
| 141 | } | |
| 142 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (sourceType == RosterSourceType.UCSB_EGRADES) { |
| 143 |
1
1. uploadRosterStudentsCSV : removed call to com/opencsv/CSVReader::skip → KILLED |
csvReader.skip(1); |
| 144 | } | |
| 145 | List<String[]> myEntries = csvReader.readAll(); | |
| 146 | for (String[] row : myEntries) { | |
| 147 | RosterStudent rosterStudent = fromCSVRow(row, sourceType); | |
| 148 | UpsertResponse upsertResponse = | |
| 149 | RosterStudentsController.upsertStudent(rosterStudent, course, RosterStatus.ROSTER); | |
| 150 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (upsertResponse.getInsertStatus() == InsertStatus.REJECTED) { |
| 151 | rejectedStudents.add(upsertResponse.rosterStudent()); | |
| 152 | } else { | |
| 153 | InsertStatus s = upsertResponse.getInsertStatus(); | |
| 154 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (s == InsertStatus.INSERTED) { |
| 155 | course.getRosterStudents().add(upsertResponse.rosterStudent()); | |
| 156 | } | |
| 157 |
1
1. uploadRosterStudentsCSV : Replaced integer addition with subtraction → KILLED |
counts[s.ordinal()]++; |
| 158 | } | |
| 159 | } | |
| 160 | } | |
| 161 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (rejectedStudents.isEmpty()) { |
| 162 | List<RosterStudent> droppedStudents = | |
| 163 | course.getRosterStudents().stream() | |
| 164 |
2
1. lambda$uploadRosterStudentsCSV$3 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::lambda$uploadRosterStudentsCSV$3 → KILLED 2. lambda$uploadRosterStudentsCSV$3 : negated conditional → KILLED |
.filter(student -> student.getRosterStatus() == RosterStatus.DROPPED) |
| 165 | .toList(); | |
| 166 | LoadResult successfulResult = | |
| 167 | new LoadResult( | |
| 168 | counts[InsertStatus.INSERTED.ordinal()], | |
| 169 | counts[InsertStatus.UPDATED.ordinal()], | |
| 170 | droppedStudents.size(), | |
| 171 | List.of()); | |
| 172 | rosterStudentRepository.saveAll(course.getRosterStudents()); | |
| 173 |
1
1. uploadRosterStudentsCSV : removed call to edu/ucsb/cs/scaffold/services/UpdateUserService::attachUsersToRosterStudents → KILLED |
updateUserService.attachUsersToRosterStudents(course.getRosterStudents()); |
| 174 |
1
1. uploadRosterStudentsCSV : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::uploadRosterStudentsCSV → KILLED |
return ResponseEntity.ok(successfulResult); |
| 175 | } else { | |
| 176 | LoadResult conflictResult = new LoadResult(0, 0, 0, rejectedStudents); | |
| 177 |
1
1. uploadRosterStudentsCSV : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::uploadRosterStudentsCSV → KILLED |
return ResponseEntity.status(HttpStatus.CONFLICT).body(conflictResult); |
| 178 | } | |
| 179 | } | |
| 180 | ||
| 181 | public static RosterStudent fromCSVRow(String[] row, RosterSourceType sourceType) { | |
| 182 |
1
1. fromCSVRow : negated conditional → KILLED |
if (sourceType == RosterSourceType.UCSB_EGRADES) { |
| 183 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromCSVRow → KILLED |
return fromUCSBEgradesCSVRow(row); |
| 184 |
1
1. fromCSVRow : negated conditional → KILLED |
} else if (sourceType == RosterSourceType.CHICO_CANVAS) { |
| 185 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromCSVRow → KILLED |
return fromChicoCanvasCSVRow(row); |
| 186 |
1
1. fromCSVRow : negated conditional → KILLED |
} else if (sourceType == RosterSourceType.OREGON_STATE) { |
| 187 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromCSVRow → KILLED |
return fromOregonStateCSVRow(row); |
| 188 |
1
1. fromCSVRow : negated conditional → KILLED |
} else if (sourceType == RosterSourceType.ROSTER_DOWNLOAD) { |
| 189 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromCSVRow → KILLED |
return fromRosterDownloadCSVRow(row); |
| 190 | } else { | |
| 191 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "CSV format not recognized"); | |
| 192 | } | |
| 193 | } | |
| 194 | ||
| 195 | public static void checkRowLength(String[] row, int expectedLength, RosterSourceType sourceType) { | |
| 196 |
2
1. checkRowLength : negated conditional → KILLED 2. checkRowLength : changed conditional boundary → KILLED |
if (row.length < expectedLength) { |
| 197 | throw new ResponseStatusException( | |
| 198 | HttpStatus.BAD_REQUEST, | |
| 199 | String.format( | |
| 200 | "%s CSV row does not have enough columns. Length = %d Row content = [%s]", | |
| 201 | sourceType.toString(), row.length, Arrays.toString(row))); | |
| 202 | } | |
| 203 | } | |
| 204 | ||
| 205 | public static RosterStudent fromUCSBEgradesCSVRow(String[] row) { | |
| 206 |
1
1. fromUCSBEgradesCSVRow : removed call to edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::checkRowLength → KILLED |
checkRowLength(row, 11, RosterSourceType.UCSB_EGRADES); |
| 207 |
1
1. fromUCSBEgradesCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromUCSBEgradesCSVRow → KILLED |
return RosterStudent.builder() |
| 208 | .firstName(row[5]) | |
| 209 | .lastName(row[4]) | |
| 210 | .studentId(row[1]) | |
| 211 | .email(row[10].strip()) | |
| 212 | .section(row[0]) | |
| 213 | .build(); | |
| 214 | } | |
| 215 | ||
| 216 | public static RosterStudent fromChicoCanvasCSVRow(String[] row) { | |
| 217 |
1
1. fromChicoCanvasCSVRow : removed call to edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::checkRowLength → KILLED |
checkRowLength(row, 4, RosterSourceType.CHICO_CANVAS); |
| 218 |
1
1. fromChicoCanvasCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromChicoCanvasCSVRow → KILLED |
return RosterStudent.builder() |
| 219 | .firstName(getFirstName(row[0])) | |
| 220 | .lastName(getLastName(row[0])) | |
| 221 | .studentId(row[2]) | |
| 222 | .email(row[3].strip()) | |
| 223 | .section("") | |
| 224 | .build(); | |
| 225 | } | |
| 226 | ||
| 227 | public static RosterStudent fromOregonStateCSVRow(String[] row) { | |
| 228 | ||
| 229 |
1
1. fromOregonStateCSVRow : removed call to edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::checkRowLength → KILLED |
checkRowLength(row, 10, RosterSourceType.OREGON_STATE); |
| 230 | String sortableName = row[1]; | |
| 231 | String sortableNameParts[] = sortableName.split(","); | |
| 232 | String lastName = sortableNameParts[0].trim(); | |
| 233 |
2
1. fromOregonStateCSVRow : negated conditional → KILLED 2. fromOregonStateCSVRow : changed conditional boundary → KILLED |
String firstName = sortableNameParts.length > 1 ? sortableNameParts[1].trim() : ""; |
| 234 |
1
1. fromOregonStateCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromOregonStateCSVRow → KILLED |
return RosterStudent.builder() |
| 235 | .firstName(firstName) | |
| 236 | .lastName(lastName) | |
| 237 | .studentId(row[9]) | |
| 238 | .email(row[8].strip()) | |
| 239 | .section("") | |
| 240 | .build(); | |
| 241 | } | |
| 242 | ||
| 243 | public static RosterStudent fromRosterDownloadCSVRow(String[] row) { | |
| 244 | // Header order: COURSEID, EMAIL, FIRSTNAME, GITHUBID, GITHUBLOGIN, ID, LASTNAME, | |
| 245 | // ORGSTATUS, ROSTERSTATUS, SECTION, STUDENTID, TEAMS, USERID | |
| 246 |
1
1. fromRosterDownloadCSVRow : removed call to edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::checkRowLength → KILLED |
checkRowLength(row, 13, RosterSourceType.ROSTER_DOWNLOAD); |
| 247 |
1
1. fromRosterDownloadCSVRow : replaced return value with null for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::fromRosterDownloadCSVRow → KILLED |
return RosterStudent.builder() |
| 248 | .firstName(row[2]) | |
| 249 | .lastName(row[6]) | |
| 250 | .studentId(row[10]) | |
| 251 | .email(row[1].strip()) | |
| 252 | .section(row[9]) | |
| 253 | .build(); | |
| 254 | } | |
| 255 | ||
| 256 | /** | |
| 257 | * Get everything except up to and not including the last space in the full name. If the string | |
| 258 | * contains no spaces, return an empty string. | |
| 259 | * | |
| 260 | * @param fullName | |
| 261 | * @return | |
| 262 | */ | |
| 263 | public static String getFirstName(String fullName) { | |
| 264 | int lastSpaceIndex = fullName.lastIndexOf(" "); | |
| 265 |
1
1. getFirstName : negated conditional → KILLED |
if (lastSpaceIndex == -1) { |
| 266 | return ""; // No spaces found, return empty string | |
| 267 | } | |
| 268 |
1
1. getFirstName : replaced return value with "" for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::getFirstName → KILLED |
return fullName.substring(0, lastSpaceIndex).trim(); // Return everything before the last space |
| 269 | } | |
| 270 | ||
| 271 | /** | |
| 272 | * Get everything after the last space in the full name. If the string contains no spaces, return | |
| 273 | * the entire input string as the result. | |
| 274 | * | |
| 275 | * @param fullName | |
| 276 | * @return best estimate of last name | |
| 277 | */ | |
| 278 | public static String getLastName(String fullName) { | |
| 279 | int lastSpaceIndex = fullName.lastIndexOf(" "); | |
| 280 |
1
1. getLastName : negated conditional → KILLED |
if (lastSpaceIndex == -1) { |
| 281 |
1
1. getLastName : replaced return value with "" for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::getLastName → KILLED |
return fullName; // No spaces found, return the entire string |
| 282 | } | |
| 283 |
2
1. getLastName : Replaced integer addition with subtraction → KILLED 2. getLastName : replaced return value with "" for edu/ucsb/cs/scaffold/controller/RosterStudentsCSVController::getLastName → KILLED |
return fullName.substring(lastSpaceIndex + 1).trim(); // Return everything after the last space |
| 284 | } | |
| 285 | } | |
Mutations | ||
| 83 |
1.1 2.2 |
|
| 85 |
1.1 2.2 |
|
| 86 |
1.1 |
|
| 91 |
1.1 |
|
| 92 |
1.1 |
|
| 97 |
1.1 |
|
| 124 |
1.1 |
|
| 127 |
1.1 2.2 |
|
| 128 |
1.1 2.2 |
|
| 139 |
1.1 |
|
| 142 |
1.1 |
|
| 143 |
1.1 |
|
| 150 |
1.1 |
|
| 154 |
1.1 |
|
| 157 |
1.1 |
|
| 161 |
1.1 |
|
| 164 |
1.1 2.2 |
|
| 173 |
1.1 |
|
| 174 |
1.1 |
|
| 177 |
1.1 |
|
| 182 |
1.1 |
|
| 183 |
1.1 |
|
| 184 |
1.1 |
|
| 185 |
1.1 |
|
| 186 |
1.1 |
|
| 187 |
1.1 |
|
| 188 |
1.1 |
|
| 189 |
1.1 |
|
| 196 |
1.1 2.2 |
|
| 206 |
1.1 |
|
| 207 |
1.1 |
|
| 217 |
1.1 |
|
| 218 |
1.1 |
|
| 229 |
1.1 |
|
| 233 |
1.1 2.2 |
|
| 234 |
1.1 |
|
| 246 |
1.1 |
|
| 247 |
1.1 |
|
| 265 |
1.1 |
|
| 268 |
1.1 |
|
| 280 |
1.1 |
|
| 281 |
1.1 |
|
| 283 |
1.1 2.2 |