1 | package edu.ucsb.cs156.frontiers.controllers; | |
2 | ||
3 | import com.opencsv.CSVReader; | |
4 | import com.opencsv.exceptions.CsvException; | |
5 | import edu.ucsb.cs156.frontiers.entities.Course; | |
6 | import edu.ucsb.cs156.frontiers.entities.RosterStudent; | |
7 | import edu.ucsb.cs156.frontiers.entities.Team; | |
8 | import edu.ucsb.cs156.frontiers.entities.TeamMember; | |
9 | import edu.ucsb.cs156.frontiers.errors.EntityNotFoundException; | |
10 | import edu.ucsb.cs156.frontiers.repositories.CourseRepository; | |
11 | import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository; | |
12 | import edu.ucsb.cs156.frontiers.repositories.TeamMemberRepository; | |
13 | import edu.ucsb.cs156.frontiers.repositories.TeamRepository; | |
14 | import io.swagger.v3.oas.annotations.Operation; | |
15 | import io.swagger.v3.oas.annotations.Parameter; | |
16 | import io.swagger.v3.oas.annotations.tags.Tag; | |
17 | import java.io.BufferedInputStream; | |
18 | import java.io.IOException; | |
19 | import java.io.InputStream; | |
20 | import java.io.InputStreamReader; | |
21 | import java.util.*; | |
22 | import lombok.extern.slf4j.Slf4j; | |
23 | import org.springframework.beans.factory.annotation.Autowired; | |
24 | import org.springframework.http.HttpStatus; | |
25 | import org.springframework.security.access.prepost.PreAuthorize; | |
26 | import org.springframework.transaction.annotation.Transactional; | |
27 | import org.springframework.web.bind.annotation.*; | |
28 | import org.springframework.web.multipart.MultipartFile; | |
29 | import org.springframework.web.server.ResponseStatusException; | |
30 | ||
31 | @Tag(name = "Teams") | |
32 | @RequestMapping("/api/teams") | |
33 | @RestController | |
34 | @Slf4j | |
35 | public class TeamsController extends ApiController { | |
36 | ||
37 | @Autowired private TeamRepository teamRepository; | |
38 | ||
39 | @Autowired private TeamMemberRepository teamMemberRepository; | |
40 | ||
41 | @Autowired private CourseRepository courseRepository; | |
42 | ||
43 | @Autowired private RosterStudentRepository rosterStudentRepository; | |
44 | ||
45 | public record TeamMemberResult( | |
46 | TeamMember teamMember, TeamMemberStatus status, String rejectedEmail) { | |
47 | public TeamMemberResult(TeamMember teamMember, TeamMemberStatus status) { | |
48 | this(teamMember, status, null); | |
49 | } | |
50 | ||
51 | public TeamMemberResult(String rejectedEmail) { | |
52 | this(null, TeamMemberStatus.MISSING, rejectedEmail); | |
53 | } | |
54 | } | |
55 | ||
56 | public record TeamCreationResponse( | |
57 | TeamSourceType typeMatched, Integer created, Integer existing, List<String> rejected) {} | |
58 | ||
59 | public record TeamMemberMapping( | |
60 | Long teamId, | |
61 | String teamName, | |
62 | Long rosterStudentId, | |
63 | String email, | |
64 | String firstName, | |
65 | String lastName, | |
66 | String githubLogin) { | |
67 | public static TeamMemberMapping from(TeamMember member) { | |
68 |
1
1. from : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController$TeamMemberMapping::from → KILLED |
return new TeamMemberMapping( |
69 | member.getTeam().getId(), | |
70 | member.getTeam().getName(), | |
71 | member.getRosterStudent().getId(), | |
72 | member.getRosterStudent().getEmail(), | |
73 | member.getRosterStudent().getFirstName(), | |
74 | member.getRosterStudent().getLastName(), | |
75 | member.getRosterStudent().getGithubLogin()); | |
76 | } | |
77 | } | |
78 | ||
79 | /** | |
80 | * This method creates a new Team. | |
81 | * | |
82 | * @param name the name of the team | |
83 | * @param courseId the ID of the course this team belongs to | |
84 | * @return the created team | |
85 | */ | |
86 | @Operation(summary = "Create a new team") | |
87 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
88 | @PostMapping("/post") | |
89 | public Team postTeam( | |
90 | @Parameter(name = "name") @RequestParam String name, | |
91 | @Parameter(name = "courseId") @RequestParam Long courseId) { | |
92 | ||
93 | Course course = | |
94 | courseRepository | |
95 | .findById(courseId) | |
96 |
1
1. lambda$postTeam$0 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$postTeam$0 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
97 | ||
98 | Team team = Team.builder().name(name).course(course).build(); | |
99 | ||
100 |
1
1. postTeam : negated conditional → KILLED |
if (teamRepository.findByCourseIdAndName(course.getId(), name).isPresent()) { |
101 | throw new ResponseStatusException( | |
102 | HttpStatus.CONFLICT, "Team with name %s already exists".formatted(name)); | |
103 | } else { | |
104 | team = teamRepository.save(team); | |
105 | } | |
106 | ||
107 |
1
1. postTeam : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::postTeam → KILLED |
return team; |
108 | } | |
109 | ||
110 | /** | |
111 | * Upload teams in CSV format (team, email) It is important to keep the code in this method | |
112 | * consistent with the code for adding a single roster student | |
113 | * | |
114 | * @param courseId course the teams are for | |
115 | * @param file csv file with roster student emails and team assignments | |
116 | * @return Count of students added to teams, already existing, and rejected students | |
117 | */ | |
118 | @Operation(summary = "Upload team assignments; CSV in format team,email") | |
119 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
120 | @PostMapping( | |
121 | value = "/upload/csv", | |
122 | consumes = {"multipart/form-data"}) | |
123 | public TeamCreationResponse uploadTeamsCsv( | |
124 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
125 | @Parameter(name = "file") @RequestParam("file") MultipartFile file) | |
126 | throws IOException, CsvException { | |
127 | ||
128 | Course course = | |
129 | courseRepository | |
130 | .findById(courseId) | |
131 |
1
1. lambda$uploadTeamsCsv$1 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$uploadTeamsCsv$1 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId.toString())); |
132 | ||
133 | int counts[] = {0, 0}; | |
134 | ||
135 | List<String> failed = new ArrayList<>(); | |
136 | ||
137 | try (InputStream inputStream = new BufferedInputStream(file.getInputStream()); | |
138 | InputStreamReader reader = new InputStreamReader(inputStream); | |
139 | CSVReader csvReader = new CSVReader(reader); ) { | |
140 | ||
141 | String[] headers = csvReader.readNext(); | |
142 | TeamSourceType sourceType = getRosterSourceType(headers); | |
143 | List<String[]> myEntries = csvReader.readAll(); | |
144 | for (String[] row : myEntries) { | |
145 | TeamMemberResult rowResult = fromCSVRow(row, sourceType, course); | |
146 |
1
1. uploadTeamsCsv : negated conditional → KILLED |
if (rowResult.status == TeamMemberStatus.MISSING) { |
147 | failed.add(rowResult.rejectedEmail); | |
148 | } else { | |
149 |
1
1. uploadTeamsCsv : Replaced integer addition with subtraction → KILLED |
counts[rowResult.status.ordinal()]++; |
150 | } | |
151 | } | |
152 | return new TeamCreationResponse( | |
153 | sourceType, | |
154 | counts[TeamMemberStatus.CREATED.ordinal()], | |
155 | counts[TeamMemberStatus.EXISTS.ordinal()], | |
156 | failed); | |
157 | } | |
158 | } | |
159 | ||
160 | /** | |
161 | * This method returns a list of all teams for a course | |
162 | * | |
163 | * @return a list of all teams for a course | |
164 | */ | |
165 | @Operation(summary = "List all teams") | |
166 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
167 | @GetMapping("/all") | |
168 | public Iterable<Team> allTeams(@RequestParam Long courseId) { | |
169 | Iterable<Team> teams = teamRepository.findByCourseId(courseId); | |
170 |
1
1. allTeams : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/TeamsController::allTeams → KILLED |
return teams; |
171 | } | |
172 | ||
173 | /** | |
174 | * Retrieves a list of mappings between roster students and teams for a given course. Each mapping | |
175 | * represents a relationship between a team and its members. | |
176 | * | |
177 | * @param courseId the unique identifier of the course for which team mappings are retrieved | |
178 | * @return an iterable collection of {@code TeamMemberMapping} objects representing the mappings | |
179 | */ | |
180 | @Operation(summary = "List the mapping of Roster Students to Teams") | |
181 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
182 | @GetMapping("/mapping") | |
183 | public Iterable<TeamMemberMapping> teamMemberMapping(@RequestParam Long courseId) { | |
184 | List<Team> teams = | |
185 | courseRepository | |
186 | .findById(courseId) | |
187 |
1
1. lambda$teamMemberMapping$2 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$teamMemberMapping$2 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)) |
188 | .getTeams(); | |
189 | List<TeamMemberMapping> mappings = new ArrayList<>(); | |
190 | for (Team team : teams) { | |
191 | for (TeamMember member : team.getTeamMembers()) { | |
192 | mappings.add(TeamMemberMapping.from(member)); | |
193 | } | |
194 | } | |
195 |
1
1. teamMemberMapping : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/controllers/TeamsController::teamMemberMapping → KILLED |
return mappings; |
196 | } | |
197 | ||
198 | /** | |
199 | * This method returns a single team by its id | |
200 | * | |
201 | * @param id the id of the team | |
202 | * @return the team | |
203 | */ | |
204 | @Operation(summary = "Get a single team") | |
205 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
206 | @GetMapping("") | |
207 | public Team getTeamById( | |
208 | @Parameter(name = "id") @RequestParam Long id, @RequestParam Long courseId) { | |
209 | Team team = | |
210 |
1
1. lambda$getTeamById$3 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$getTeamById$3 → KILLED |
teamRepository.findById(id).orElseThrow(() -> new EntityNotFoundException(Team.class, id)); |
211 |
1
1. getTeamById : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::getTeamById → KILLED |
return team; |
212 | } | |
213 | ||
214 | /** | |
215 | * This method deletes a team by its id | |
216 | * | |
217 | * @param id the id of the team to delete | |
218 | * @return a message indicating the team was deleted | |
219 | */ | |
220 | @Operation(summary = "Delete a team") | |
221 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
222 | @DeleteMapping("") | |
223 | public Object deleteTeam( | |
224 | @Parameter(name = "id") @RequestParam Long id, @RequestParam Long courseId) { | |
225 | Team team = | |
226 |
1
1. lambda$deleteTeam$4 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$deleteTeam$4 → KILLED |
teamRepository.findById(id).orElseThrow(() -> new EntityNotFoundException(Team.class, id)); |
227 |
1
1. deleteTeam : removed call to edu/ucsb/cs156/frontiers/repositories/TeamRepository::delete → KILLED |
teamRepository.delete(team); |
228 |
1
1. deleteTeam : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::deleteTeam → KILLED |
return genericMessage("Team with id %s deleted".formatted(id)); |
229 | } | |
230 | ||
231 | /** | |
232 | * This method adds a roster student as a team member | |
233 | * | |
234 | * @param teamId the ID of the team | |
235 | * @param rosterStudentId the ID of the roster student to add | |
236 | * @return the created team member | |
237 | */ | |
238 | @Operation(summary = "Add a roster student to a team") | |
239 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
240 | @PostMapping("/addMember") | |
241 | public TeamMember addTeamMember( | |
242 | @Parameter(name = "teamId") @RequestParam Long teamId, | |
243 | @Parameter(name = "rosterStudentId") @RequestParam Long rosterStudentId, | |
244 | @Parameter(name = "courseId") @RequestParam Long courseId) { | |
245 | ||
246 | Team team = | |
247 | teamRepository | |
248 | .findById(teamId) | |
249 |
1
1. lambda$addTeamMember$5 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$addTeamMember$5 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Team.class, teamId)); |
250 | ||
251 | RosterStudent rosterStudent = | |
252 | rosterStudentRepository | |
253 | .findById(rosterStudentId) | |
254 |
1
1. lambda$addTeamMember$6 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$addTeamMember$6 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(RosterStudent.class, rosterStudentId)); |
255 | ||
256 |
1
1. addTeamMember : negated conditional → KILLED |
if (!team.getCourse().getId().equals(courseId)) { |
257 | throw new ResponseStatusException( | |
258 | HttpStatus.BAD_REQUEST, "Team is not from course %d".formatted(courseId)); | |
259 | } | |
260 |
1
1. addTeamMember : negated conditional → KILLED |
if (!rosterStudent.getCourse().getId().equals(courseId)) { |
261 | throw new ResponseStatusException( | |
262 | HttpStatus.BAD_REQUEST, "Roster student is not from course %d".formatted(courseId)); | |
263 | } | |
264 | ||
265 |
1
1. addTeamMember : negated conditional → KILLED |
if (teamMemberRepository.findByTeamAndRosterStudent(team, rosterStudent).isPresent()) { |
266 | throw new ResponseStatusException( | |
267 | HttpStatus.CONFLICT, | |
268 | "Team member already exists for team %s and roster student %s" | |
269 | .formatted(team.getName(), rosterStudent.getEmail())); | |
270 | } | |
271 | TeamMember teamMember = TeamMember.builder().team(team).rosterStudent(rosterStudent).build(); | |
272 | TeamMember savedTeamMember = teamMemberRepository.save(teamMember); | |
273 | ||
274 |
1
1. addTeamMember : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::addTeamMember → KILLED |
return savedTeamMember; |
275 | } | |
276 | ||
277 | /** | |
278 | * This method removes a team member | |
279 | * | |
280 | * @param teamMemberId the ID of the team member to remove | |
281 | * @return a message indicating the team member was removed | |
282 | */ | |
283 | @Operation(summary = "Remove a team member") | |
284 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
285 | @DeleteMapping("/removeMember") | |
286 | @Transactional | |
287 | public Object removeTeamMember( | |
288 | @Parameter(name = "teamMemberId") @RequestParam Long teamMemberId, | |
289 | @Parameter(name = "courseId") @RequestParam Long courseId) { | |
290 | TeamMember teamMember = | |
291 | teamMemberRepository | |
292 | .findById(teamMemberId) | |
293 |
1
1. lambda$removeTeamMember$7 : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::lambda$removeTeamMember$7 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(TeamMember.class, teamMemberId)); |
294 | Team team = teamMember.getTeam(); | |
295 | RosterStudent rosterStudent = teamMember.getRosterStudent(); | |
296 | team.getTeamMembers().remove(teamMember); | |
297 | rosterStudent.getTeamMembers().remove(teamMember); | |
298 |
1
1. removeTeamMember : removed call to edu/ucsb/cs156/frontiers/repositories/TeamMemberRepository::delete → KILLED |
teamMemberRepository.delete(teamMember); |
299 | teamRepository.save(team); | |
300 | rosterStudentRepository.save(rosterStudent); | |
301 |
1
1. removeTeamMember : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::removeTeamMember → KILLED |
return genericMessage("Team member with id %s deleted".formatted(teamMemberId)); |
302 | } | |
303 | ||
304 | public enum TeamSourceType { | |
305 | SIMPLE, | |
306 | UNKNOWN | |
307 | } | |
308 | ||
309 | public enum TeamMemberStatus { | |
310 | CREATED, | |
311 | EXISTS, | |
312 | MISSING | |
313 | } | |
314 | ||
315 | public static final String SIMPLE_HEADERS = "team,email"; | |
316 | ||
317 | public TeamSourceType getRosterSourceType(String[] headers) { | |
318 | ||
319 | Map<TeamSourceType, String[]> sourceTypeToHeaders = new HashMap<>(); | |
320 | ||
321 | sourceTypeToHeaders.put(TeamSourceType.SIMPLE, SIMPLE_HEADERS.split(",")); | |
322 | ||
323 | for (Map.Entry<TeamSourceType, String[]> entry : sourceTypeToHeaders.entrySet()) { | |
324 | TeamSourceType type = entry.getKey(); | |
325 | String[] expectedHeaders = entry.getValue(); | |
326 |
2
1. getRosterSourceType : negated conditional → KILLED 2. getRosterSourceType : changed conditional boundary → KILLED |
if (headers.length >= expectedHeaders.length) { |
327 | boolean matches = true; | |
328 |
2
1. getRosterSourceType : changed conditional boundary → KILLED 2. getRosterSourceType : negated conditional → KILLED |
for (int i = 0; i < expectedHeaders.length; i++) { |
329 |
1
1. getRosterSourceType : negated conditional → KILLED |
if (!expectedHeaders[i].equalsIgnoreCase(headers[i])) { |
330 | matches = false; | |
331 | break; | |
332 | } | |
333 | } | |
334 |
1
1. getRosterSourceType : negated conditional → KILLED |
if (matches) { |
335 |
1
1. getRosterSourceType : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::getRosterSourceType → KILLED |
return type; |
336 | } | |
337 | } | |
338 | } | |
339 | // If no known type matches, throw | |
340 | throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown Roster Source Type"); | |
341 | } | |
342 | ||
343 | public TeamMemberResult fromCSVRow(String[] row, TeamSourceType sourceType, Course course) { | |
344 | // No if statements because this is the only possible value to enter here at the moment. Replace | |
345 | // with if when more | |
346 | // Formats are added. | |
347 |
1
1. fromCSVRow : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::fromCSVRow → KILLED |
return teamMemberFromSimpleCsv(row, course); |
348 | } | |
349 | ||
350 | public TeamMemberResult teamMemberFromSimpleCsv(String[] row, Course course) { | |
351 | Optional<RosterStudent> student = | |
352 | rosterStudentRepository.findByCourseIdAndEmail(course.getId(), row[1]); | |
353 | Optional<Team> team = teamRepository.findByCourseIdAndName(course.getId(), row[0]); | |
354 |
2
1. teamMemberFromSimpleCsv : negated conditional → KILLED 2. teamMemberFromSimpleCsv : negated conditional → KILLED |
if (student.isPresent() && team.isPresent()) { |
355 | Optional<TeamMember> teamMember = | |
356 | teamMemberRepository.findByTeamAndRosterStudent(team.get(), student.get()); | |
357 |
1
1. teamMemberFromSimpleCsv : negated conditional → KILLED |
if (teamMember.isPresent()) { |
358 |
1
1. teamMemberFromSimpleCsv : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::teamMemberFromSimpleCsv → KILLED |
return new TeamMemberResult(teamMember.get(), TeamMemberStatus.EXISTS); |
359 | } else { | |
360 | TeamMember teamMemberToSave = | |
361 | TeamMember.builder().team(team.get()).rosterStudent(student.get()).build(); | |
362 | TeamMember savedTeamMember = teamMemberRepository.save(teamMemberToSave); | |
363 |
1
1. teamMemberFromSimpleCsv : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::teamMemberFromSimpleCsv → KILLED |
return new TeamMemberResult(savedTeamMember, TeamMemberStatus.CREATED); |
364 | } | |
365 |
1
1. teamMemberFromSimpleCsv : negated conditional → KILLED |
} else if (student.isPresent()) { |
366 | Team teamToSave = Team.builder().name(row[0]).course(course).build(); | |
367 | teamRepository.save(teamToSave); | |
368 | TeamMember saveTeamMember = | |
369 | TeamMember.builder().team(teamToSave).rosterStudent(student.get()).build(); | |
370 | teamMemberRepository.save(saveTeamMember); | |
371 |
1
1. teamMemberFromSimpleCsv : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::teamMemberFromSimpleCsv → KILLED |
return new TeamMemberResult(saveTeamMember, TeamMemberStatus.CREATED); |
372 | } else { | |
373 |
1
1. teamMemberFromSimpleCsv : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/TeamsController::teamMemberFromSimpleCsv → KILLED |
return new TeamMemberResult(row[1]); |
374 | } | |
375 | } | |
376 | } | |
Mutations | ||
68 |
1.1 |
|
96 |
1.1 |
|
100 |
1.1 |
|
107 |
1.1 |
|
131 |
1.1 |
|
146 |
1.1 |
|
149 |
1.1 |
|
170 |
1.1 |
|
187 |
1.1 |
|
195 |
1.1 |
|
210 |
1.1 |
|
211 |
1.1 |
|
226 |
1.1 |
|
227 |
1.1 |
|
228 |
1.1 |
|
249 |
1.1 |
|
254 |
1.1 |
|
256 |
1.1 |
|
260 |
1.1 |
|
265 |
1.1 |
|
274 |
1.1 |
|
293 |
1.1 |
|
298 |
1.1 |
|
301 |
1.1 |
|
326 |
1.1 2.2 |
|
328 |
1.1 2.2 |
|
329 |
1.1 |
|
334 |
1.1 |
|
335 |
1.1 |
|
347 |
1.1 |
|
354 |
1.1 2.2 |
|
357 |
1.1 |
|
358 |
1.1 |
|
363 |
1.1 |
|
365 |
1.1 |
|
371 |
1.1 |
|
373 |
1.1 |