CanvasController.java
package edu.ucsb.cs156.frontiers.controllers;
import static edu.ucsb.cs156.frontiers.controllers.RosterStudentsController.upsertStudent;
import edu.ucsb.cs156.frontiers.entities.Course;
import edu.ucsb.cs156.frontiers.entities.Job;
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
import edu.ucsb.cs156.frontiers.enums.InsertStatus;
import edu.ucsb.cs156.frontiers.enums.RosterStatus;
import edu.ucsb.cs156.frontiers.errors.EntityNotFoundException;
import edu.ucsb.cs156.frontiers.jobs.PullTeamsFromCanvasJob;
import edu.ucsb.cs156.frontiers.jobs.RemoveStudentsJob;
import edu.ucsb.cs156.frontiers.models.CanvasGroupSet;
import edu.ucsb.cs156.frontiers.models.LoadResult;
import edu.ucsb.cs156.frontiers.models.UpsertResponse;
import edu.ucsb.cs156.frontiers.repositories.CourseRepository;
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
import edu.ucsb.cs156.frontiers.repositories.TeamMemberRepository;
import edu.ucsb.cs156.frontiers.repositories.TeamRepository;
import edu.ucsb.cs156.frontiers.services.CanvasService;
import edu.ucsb.cs156.frontiers.services.OrganizationMemberService;
import edu.ucsb.cs156.frontiers.services.UpdateUserService;
import edu.ucsb.cs156.frontiers.services.jobs.JobService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/courses/canvas")
@Tag(name = "Canvas")
@Slf4j
public class CanvasController extends ApiController {
private final CourseRepository courseRepository;
private final CanvasService canvasService;
private final TeamRepository teamRepository;
private final JobService jobService;
private final RosterStudentRepository rosterStudentRepository;
private final UpdateUserService updateUserService;
private final OrganizationMemberService organizationMemberService;
private final TeamMemberRepository teamMemberRepository;
public CanvasController(
CourseRepository courseRepository,
CanvasService canvasService,
TeamRepository teamRepository,
JobService jobService,
RosterStudentRepository rosterStudentRepository,
UpdateUserService updateUserService,
OrganizationMemberService organizationMemberService,
TeamMemberRepository teamMemberRepository) {
this.courseRepository = courseRepository;
this.canvasService = canvasService;
this.teamRepository = teamRepository;
this.jobService = jobService;
this.rosterStudentRepository = rosterStudentRepository;
this.updateUserService = updateUserService;
this.organizationMemberService = organizationMemberService;
this.teamMemberRepository = teamMemberRepository;
}
/**
* Upload Roster students for Course from Canvas. It is important to keep the code in this method
* consistent with the code in uploadRosterStudentsCSV.
*
* @param courseId the internal course ID in Frontiers
* @return LoadResult with counts of inserted, updated, dropped students and any rejected students
*/
@Operation(summary = "Upload Roster students for Course from Canvas")
@PreAuthorize("@CourseSecurity.hasInstructorPermissions(#root, #courseId)")
@PostMapping("/sync/students")
public ResponseEntity<LoadResult> uploadRosterFromCanvas(
@Parameter(name = "courseId") @RequestParam Long courseId) {
Course course =
courseRepository
.findById(courseId)
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId.toString()));
course.getRosterStudents().stream()
.filter(filteredStudent -> filteredStudent.getRosterStatus() == RosterStatus.ROSTER)
.forEach(student -> student.setRosterStatus(RosterStatus.DROPPED));
int counts[] = {0, 0};
List<RosterStudent> rejectedStudents = new ArrayList<>();
List<RosterStudent> canvasStudents = canvasService.getCanvasRoster(course);
for (RosterStudent rosterStudent : canvasStudents) {
UpsertResponse upsertResponse = upsertStudent(rosterStudent, course, RosterStatus.ROSTER);
if (upsertResponse.getInsertStatus() == InsertStatus.REJECTED) {
rejectedStudents.add(upsertResponse.rosterStudent());
} else {
InsertStatus s = upsertResponse.getInsertStatus();
if (s == InsertStatus.INSERTED) {
course.getRosterStudents().add(upsertResponse.rosterStudent());
}
counts[s.ordinal()]++;
}
}
if (rejectedStudents.isEmpty()) {
List<RosterStudent> droppedStudents =
course.getRosterStudents().stream()
.filter(student -> student.getRosterStatus() == RosterStatus.DROPPED)
.toList();
LoadResult successfulResult =
new LoadResult(
counts[InsertStatus.INSERTED.ordinal()],
counts[InsertStatus.UPDATED.ordinal()],
droppedStudents.size(),
List.of());
rosterStudentRepository.saveAll(course.getRosterStudents());
updateUserService.attachUsersToRosterStudents(course.getRosterStudents());
RemoveStudentsJob job =
RemoveStudentsJob.builder()
.students(droppedStudents)
.organizationMemberService(organizationMemberService)
.rosterStudentRepository(rosterStudentRepository)
.build();
jobService.runAsJob(job);
return ResponseEntity.ok(successfulResult);
} else {
LoadResult conflictResult = new LoadResult(0, 0, 0, rejectedStudents);
return ResponseEntity.status(HttpStatus.CONFLICT).body(conflictResult);
}
}
@Operation(summary = "See available Canvas GroupSets")
@GetMapping("/groupsets")
@PreAuthorize("@CourseSecurity.hasInstructorPermissions(#root, #courseId)")
public List<CanvasGroupSet> getCanvasGroupSets(
@Parameter(name = "courseId") @RequestParam Long courseId) {
Course course =
courseRepository
.findById(courseId)
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId.toString()));
return canvasService.getCanvasGroupSets(course);
}
@Operation(summary = "Load Groups from Canvas")
@PostMapping("/sync/teams")
@PreAuthorize("@CourseSecurity.hasInstructorPermissions(#root, #courseId)")
public Job loadCanvasTeams(
@Parameter(name = "courseId") @RequestParam Long courseId,
@Parameter(name = "groupSetId") @RequestParam String groupSetId) {
Course course =
courseRepository
.findById(courseId)
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId.toString()));
PullTeamsFromCanvasJob job =
PullTeamsFromCanvasJob.builder()
.course(course)
.canvasService(canvasService)
.teamRepository(teamRepository)
.teamMemberRepository(teamMemberRepository)
.courseRepository(courseRepository)
.groupsetId(groupSetId)
.build();
return jobService.runAsJob(job);
}
}