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 | UNKNOWN | |
62 | } | |
63 | ||
64 | public static final String UCSB_EGRADES_HEADERS = | |
65 | "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"; | |
66 | public static final String CHICO_CANVAS_HEADERS = | |
67 | "Student Name,Student ID,Student SIS ID,Email,Section Name"; | |
68 | public static final String OREGON_STATE_HEADERS = | |
69 | "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"; | |
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 | ||
79 | for (Map.Entry<RosterSourceType, String[]> entry : sourceTypeToHeaders.entrySet()) { | |
80 | RosterSourceType type = entry.getKey(); | |
81 | String[] expectedHeaders = entry.getValue(); | |
82 |
2
1. getRosterSourceType : negated conditional → KILLED 2. getRosterSourceType : changed conditional boundary → KILLED |
if (headers.length >= expectedHeaders.length) { |
83 | boolean matches = true; | |
84 |
2
1. getRosterSourceType : changed conditional boundary → KILLED 2. getRosterSourceType : negated conditional → KILLED |
for (int i = 0; i < expectedHeaders.length; i++) { |
85 |
1
1. getRosterSourceType : negated conditional → KILLED |
if (!expectedHeaders[i].trim().equalsIgnoreCase(headers[i].trim())) { |
86 | matches = false; | |
87 | break; | |
88 | } | |
89 | } | |
90 |
1
1. getRosterSourceType : negated conditional → KILLED |
if (matches) { |
91 |
1
1. getRosterSourceType : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::getRosterSourceType → KILLED |
return type; |
92 | } | |
93 | } | |
94 | } | |
95 | // If no known type matches, return UNKNOWN | |
96 |
1
1. getRosterSourceType : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::getRosterSourceType → KILLED |
return RosterSourceType.UNKNOWN; |
97 | } | |
98 | ||
99 | /** | |
100 | * Upload Roster students for Course in any supported format. It is important to keep the code in | |
101 | * this method consistent with the code for adding a single roster student | |
102 | * | |
103 | * @param courseId | |
104 | * @param file | |
105 | * @return | |
106 | * @throws JsonProcessingException | |
107 | * @throws IOException | |
108 | * @throws CsvException | |
109 | */ | |
110 | @Operation(summary = "Upload Roster students for Course in any supported Format") | |
111 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
112 | @PostMapping( | |
113 | value = "/upload/csv", | |
114 | consumes = {"multipart/form-data"}) | |
115 | public ResponseEntity<LoadResult> uploadRosterStudentsCSV( | |
116 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
117 | @Parameter(name = "file") @RequestParam("file") MultipartFile file) | |
118 | throws JsonProcessingException, IOException, CsvException { | |
119 | ||
120 | Course course = | |
121 | courseRepository | |
122 | .findById(courseId) | |
123 |
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())); |
124 | ||
125 | course.getRosterStudents().stream() | |
126 |
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) |
127 |
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)); |
128 | ||
129 | int counts[] = {0, 0}; | |
130 | List<RosterStudent> rejectedStudents = new ArrayList<>(); | |
131 | ||
132 | try (InputStream inputStream = new BufferedInputStream(file.getInputStream()); | |
133 | InputStreamReader reader = new InputStreamReader(inputStream); | |
134 | CSVReader csvReader = new CSVReader(reader); ) { | |
135 | ||
136 | String[] headers = csvReader.readNext(); | |
137 | RosterSourceType sourceType = getRosterSourceType(headers); | |
138 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (sourceType == RosterSourceType.UNKNOWN) { |
139 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown Roster Source Type"); | |
140 | } | |
141 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (sourceType == RosterSourceType.UCSB_EGRADES) { |
142 |
1
1. uploadRosterStudentsCSV : removed call to com/opencsv/CSVReader::skip → KILLED |
csvReader.skip(1); |
143 | } | |
144 | List<String[]> myEntries = csvReader.readAll(); | |
145 | for (String[] row : myEntries) { | |
146 | RosterStudent rosterStudent = fromCSVRow(row, sourceType); | |
147 | UpsertResponse upsertResponse = | |
148 | RosterStudentsController.upsertStudent(rosterStudent, course, RosterStatus.ROSTER); | |
149 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (upsertResponse.getInsertStatus() == InsertStatus.REJECTED) { |
150 | rejectedStudents.add(upsertResponse.rosterStudent()); | |
151 | } else { | |
152 | InsertStatus s = upsertResponse.getInsertStatus(); | |
153 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (s == InsertStatus.INSERTED) { |
154 | course.getRosterStudents().add(upsertResponse.rosterStudent()); | |
155 | } | |
156 |
1
1. uploadRosterStudentsCSV : Replaced integer addition with subtraction → KILLED |
counts[s.ordinal()]++; |
157 | } | |
158 | } | |
159 | } | |
160 |
1
1. uploadRosterStudentsCSV : negated conditional → KILLED |
if (rejectedStudents.isEmpty()) { |
161 | List<RosterStudent> droppedStudents = | |
162 | course.getRosterStudents().stream() | |
163 |
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) |
164 | .toList(); | |
165 | LoadResult successfulResult = | |
166 | new LoadResult( | |
167 | counts[InsertStatus.INSERTED.ordinal()], | |
168 | counts[InsertStatus.UPDATED.ordinal()], | |
169 | droppedStudents.size(), | |
170 | List.of()); | |
171 | rosterStudentRepository.saveAll(course.getRosterStudents()); | |
172 |
1
1. uploadRosterStudentsCSV : removed call to edu/ucsb/cs156/frontiers/services/UpdateUserService::attachUsersToRosterStudents → KILLED |
updateUserService.attachUsersToRosterStudents(course.getRosterStudents()); |
173 | RemoveStudentsJob job = | |
174 | RemoveStudentsJob.builder() | |
175 | .students(droppedStudents) | |
176 | .organizationMemberService(organizationMemberService) | |
177 | .rosterStudentRepository(rosterStudentRepository) | |
178 | .build(); | |
179 | jobService.runAsJob(job); | |
180 |
1
1. uploadRosterStudentsCSV : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::uploadRosterStudentsCSV → KILLED |
return ResponseEntity.ok(successfulResult); |
181 | } else { | |
182 | LoadResult conflictResult = new LoadResult(0, 0, 0, rejectedStudents); | |
183 |
1
1. uploadRosterStudentsCSV : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::uploadRosterStudentsCSV → KILLED |
return ResponseEntity.status(HttpStatus.CONFLICT).body(conflictResult); |
184 | } | |
185 | } | |
186 | ||
187 | public static RosterStudent fromCSVRow(String[] row, RosterSourceType sourceType) { | |
188 |
1
1. fromCSVRow : negated conditional → KILLED |
if (sourceType == RosterSourceType.UCSB_EGRADES) { |
189 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::fromCSVRow → KILLED |
return fromUCSBEgradesCSVRow(row); |
190 |
1
1. fromCSVRow : negated conditional → KILLED |
} else if (sourceType == RosterSourceType.CHICO_CANVAS) { |
191 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::fromCSVRow → KILLED |
return fromChicoCanvasCSVRow(row); |
192 |
1
1. fromCSVRow : negated conditional → KILLED |
} else if (sourceType == RosterSourceType.OREGON_STATE) { |
193 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::fromCSVRow → KILLED |
return fromOregonStateCSVRow(row); |
194 | } else { | |
195 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "CSV format not recognized"); | |
196 | } | |
197 | } | |
198 | ||
199 | public static void checkRowLength(String[] row, int expectedLength, RosterSourceType sourceType) { | |
200 |
2
1. checkRowLength : changed conditional boundary → KILLED 2. checkRowLength : negated conditional → KILLED |
if (row.length < expectedLength) { |
201 | throw new ResponseStatusException( | |
202 | HttpStatus.BAD_REQUEST, | |
203 | String.format( | |
204 | "%s CSV row does not have enough columns. Length = %d Row content = [%s]", | |
205 | sourceType.toString(), row.length, Arrays.toString(row))); | |
206 | } | |
207 | } | |
208 | ||
209 | public static RosterStudent fromUCSBEgradesCSVRow(String[] row) { | |
210 |
1
1. fromUCSBEgradesCSVRow : removed call to edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::checkRowLength → KILLED |
checkRowLength(row, 11, RosterSourceType.UCSB_EGRADES); |
211 |
1
1. fromUCSBEgradesCSVRow : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::fromUCSBEgradesCSVRow → KILLED |
return RosterStudent.builder() |
212 | .firstName(row[5]) | |
213 | .lastName(row[4]) | |
214 | .studentId(row[1]) | |
215 | .email(row[10]) | |
216 | .section(row[0]) | |
217 | .build(); | |
218 | } | |
219 | ||
220 | public static RosterStudent fromChicoCanvasCSVRow(String[] row) { | |
221 |
1
1. fromChicoCanvasCSVRow : removed call to edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::checkRowLength → KILLED |
checkRowLength(row, 4, RosterSourceType.CHICO_CANVAS); |
222 |
1
1. fromChicoCanvasCSVRow : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::fromChicoCanvasCSVRow → KILLED |
return RosterStudent.builder() |
223 | .firstName(getFirstName(row[0])) | |
224 | .lastName(getLastName(row[0])) | |
225 | .studentId(row[2]) | |
226 | .email(row[3]) | |
227 | .section("") | |
228 | .build(); | |
229 | } | |
230 | ||
231 | public static RosterStudent fromOregonStateCSVRow(String[] row) { | |
232 | ||
233 |
1
1. fromOregonStateCSVRow : removed call to edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::checkRowLength → KILLED |
checkRowLength(row, 10, RosterSourceType.OREGON_STATE); |
234 | String sortableName = row[1]; | |
235 | String sortableNameParts[] = sortableName.split(","); | |
236 | String lastName = sortableNameParts[0].trim(); | |
237 |
2
1. fromOregonStateCSVRow : changed conditional boundary → KILLED 2. fromOregonStateCSVRow : negated conditional → KILLED |
String firstName = sortableNameParts.length > 1 ? sortableNameParts[1].trim() : ""; |
238 |
1
1. fromOregonStateCSVRow : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/RosterStudentsCSVController::fromOregonStateCSVRow → KILLED |
return RosterStudent.builder() |
239 | .firstName(firstName) | |
240 | .lastName(lastName) | |
241 | .studentId(row[9]) | |
242 | .email(row[8]) | |
243 | .section("") | |
244 | .build(); | |
245 | } | |
246 | ||
247 | /** | |
248 | * Get everything except up to and not including the last space in the full name. If the string | |
249 | * contains no spaces, return an empty string. | |
250 | * | |
251 | * @param fullName | |
252 | * @return | |
253 | */ | |
254 | public static String getFirstName(String fullName) { | |
255 | int lastSpaceIndex = fullName.lastIndexOf(" "); | |
256 |
1
1. getFirstName : negated conditional → KILLED |
if (lastSpaceIndex == -1) { |
257 | return ""; // No spaces found, return empty string | |
258 | } | |
259 |
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 |
260 | } | |
261 | ||
262 | /** | |
263 | * Get everything after the last space in the full name. If the string contains no spaces, return | |
264 | * the entire input string as the result. | |
265 | * | |
266 | * @param fullName | |
267 | * @return best estimate of last name | |
268 | */ | |
269 | public static String getLastName(String fullName) { | |
270 | int lastSpaceIndex = fullName.lastIndexOf(" "); | |
271 |
1
1. getLastName : negated conditional → KILLED |
if (lastSpaceIndex == -1) { |
272 |
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 |
273 | } | |
274 |
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 |
275 | } | |
276 | } | |
Mutations | ||
82 |
1.1 2.2 |
|
84 |
1.1 2.2 |
|
85 |
1.1 |
|
90 |
1.1 |
|
91 |
1.1 |
|
96 |
1.1 |
|
123 |
1.1 |
|
126 |
1.1 2.2 |
|
127 |
1.1 2.2 |
|
138 |
1.1 |
|
141 |
1.1 |
|
142 |
1.1 |
|
149 |
1.1 |
|
153 |
1.1 |
|
156 |
1.1 |
|
160 |
1.1 |
|
163 |
1.1 2.2 |
|
172 |
1.1 |
|
180 |
1.1 |
|
183 |
1.1 |
|
188 |
1.1 |
|
189 |
1.1 |
|
190 |
1.1 |
|
191 |
1.1 |
|
192 |
1.1 |
|
193 |
1.1 |
|
200 |
1.1 2.2 |
|
210 |
1.1 |
|
211 |
1.1 |
|
221 |
1.1 |
|
222 |
1.1 |
|
233 |
1.1 |
|
237 |
1.1 2.2 |
|
238 |
1.1 |
|
256 |
1.1 |
|
259 |
1.1 |
|
271 |
1.1 |
|
272 |
1.1 |
|
274 |
1.1 2.2 |