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