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