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 |