| 1 | package edu.ucsb.cs.scaffold.controller; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.databind.JsonNode; | |
| 4 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 5 | import edu.ucsb.cs.scaffold.entity.Course; | |
| 6 | import edu.ucsb.cs.scaffold.entity.CourseStaff; | |
| 7 | import edu.ucsb.cs.scaffold.entity.PatCredential; | |
| 8 | import edu.ucsb.cs.scaffold.entity.PlInstance; | |
| 9 | import edu.ucsb.cs.scaffold.entity.PlRepo; | |
| 10 | import edu.ucsb.cs.scaffold.entity.RosterStudent; | |
| 11 | import edu.ucsb.cs.scaffold.entity.User; | |
| 12 | import edu.ucsb.cs.scaffold.enums.PatPlatform; | |
| 13 | import edu.ucsb.cs.scaffold.enums.School; | |
| 14 | import edu.ucsb.cs.scaffold.errors.EntityNotFoundException; | |
| 15 | import edu.ucsb.cs.scaffold.errors.ForbiddenException; | |
| 16 | import edu.ucsb.cs.scaffold.jobs.SyncCourseWithPlRepoJobFactory; | |
| 17 | import edu.ucsb.cs.scaffold.model.CurrentUser; | |
| 18 | import edu.ucsb.cs.scaffold.repository.AdminRepository; | |
| 19 | import edu.ucsb.cs.scaffold.repository.CourseRepository; | |
| 20 | import edu.ucsb.cs.scaffold.repository.CourseStaffRepository; | |
| 21 | import edu.ucsb.cs.scaffold.repository.InstructorRepository; | |
| 22 | import edu.ucsb.cs.scaffold.repository.PatCredentialRepository; | |
| 23 | import edu.ucsb.cs.scaffold.repository.PlInstanceRepository; | |
| 24 | import edu.ucsb.cs.scaffold.repository.PlRepoRepository; | |
| 25 | import edu.ucsb.cs.scaffold.repository.RosterStudentRepository; | |
| 26 | import edu.ucsb.cs.scaffold.repository.UserRepository; | |
| 27 | import edu.ucsb.cs.scaffold.services.GithubService; | |
| 28 | import edu.ucsb.cs.scaffold.services.PatEncryptionService; | |
| 29 | import edu.ucsb.cs.scaffold.services.PrairieLearnService; | |
| 30 | import edu.ucsb.cs156.jobs.repositories.JobsRepository; | |
| 31 | import edu.ucsb.cs156.jobs.services.JobService; | |
| 32 | import io.swagger.v3.oas.annotations.Operation; | |
| 33 | import io.swagger.v3.oas.annotations.Parameter; | |
| 34 | import io.swagger.v3.oas.annotations.tags.Tag; | |
| 35 | import java.security.NoSuchAlgorithmException; | |
| 36 | import java.security.spec.InvalidKeySpecException; | |
| 37 | import java.util.ArrayList; | |
| 38 | import java.util.HashSet; | |
| 39 | import java.util.List; | |
| 40 | import java.util.Map; | |
| 41 | import java.util.Objects; | |
| 42 | import java.util.Set; | |
| 43 | import java.util.stream.Collectors; | |
| 44 | import java.util.stream.StreamSupport; | |
| 45 | import lombok.extern.slf4j.Slf4j; | |
| 46 | import org.springframework.beans.factory.annotation.Autowired; | |
| 47 | import org.springframework.security.access.prepost.PreAuthorize; | |
| 48 | import org.springframework.transaction.annotation.Transactional; | |
| 49 | import org.springframework.web.bind.annotation.DeleteMapping; | |
| 50 | import org.springframework.web.bind.annotation.GetMapping; | |
| 51 | import org.springframework.web.bind.annotation.PathVariable; | |
| 52 | import org.springframework.web.bind.annotation.PostMapping; | |
| 53 | import org.springframework.web.bind.annotation.PutMapping; | |
| 54 | import org.springframework.web.bind.annotation.RequestMapping; | |
| 55 | import org.springframework.web.bind.annotation.RequestParam; | |
| 56 | import org.springframework.web.bind.annotation.RestController; | |
| 57 | import org.springframework.web.client.HttpClientErrorException; | |
| 58 | ||
| 59 | @Tag(name = "Course") | |
| 60 | @RequestMapping("/api/courses") | |
| 61 | @RestController | |
| 62 | @Slf4j | |
| 63 | public class CoursesController extends ApiController { | |
| 64 | ||
| 65 | @Autowired private CourseRepository courseRepository; | |
| 66 | ||
| 67 | @Autowired private UserRepository userRepository; | |
| 68 | ||
| 69 | @Autowired private RosterStudentRepository rosterStudentRepository; | |
| 70 | ||
| 71 | @Autowired private CourseStaffRepository courseStaffRepository; | |
| 72 | ||
| 73 | @Autowired private InstructorRepository instructorRepository; | |
| 74 | ||
| 75 | @Autowired private AdminRepository adminRepository; | |
| 76 | ||
| 77 | @Autowired private JobsRepository jobsRepository; | |
| 78 | ||
| 79 | @Autowired private PatCredentialRepository patCredentialRepository; | |
| 80 | ||
| 81 | @Autowired private PatEncryptionService patEncryptionService; | |
| 82 | ||
| 83 | @Autowired private PlRepoRepository plRepoRepository; | |
| 84 | ||
| 85 | @Autowired private GithubService githubService; | |
| 86 | ||
| 87 | @Autowired private PlInstanceRepository plInstanceRepository; | |
| 88 | ||
| 89 | @Autowired private PrairieLearnService prairieLearnService; | |
| 90 | ||
| 91 | @Autowired private SyncCourseWithPlRepoJobFactory syncCourseWithPlRepoJobFactory; | |
| 92 | ||
| 93 | @Autowired private JobService jobService; | |
| 94 | ||
| 95 | /** | |
| 96 | * This method creates a new Course. | |
| 97 | * | |
| 98 | * @param courseName the name of the course | |
| 99 | * @param term the term of the course | |
| 100 | * @param school the school of the course | |
| 101 | * @param canvasApiToken the Canvas API token (optional) | |
| 102 | * @param canvasCourseId the Canvas course ID (optional) | |
| 103 | */ | |
| 104 | @Operation(summary = "Create a new course") | |
| 105 | @PreAuthorize("hasRole('ROLE_ADMIN') || hasRole('ROLE_INSTRUCTOR')") | |
| 106 | @PostMapping("/post") | |
| 107 | public InstructorCourseView postCourse( | |
| 108 | @Parameter(name = "courseName") @RequestParam String courseName, | |
| 109 | @Parameter(name = "term") @RequestParam String term, | |
| 110 | @Parameter(name = "school") @RequestParam School school, | |
| 111 | @Parameter(name = "canvasApiToken") @RequestParam(required = false) String canvasApiToken, | |
| 112 | @Parameter(name = "canvasCourseId") @RequestParam(required = false) String canvasCourseId) { | |
| 113 | // get current date right now and set status to pending | |
| 114 | edu.ucsb.cs.scaffold.model.CurrentUser currentUser = getCurrentUser(); | |
| 115 | Course course = | |
| 116 | Course.builder() | |
| 117 | .courseName(courseName) | |
| 118 | .term(term) | |
| 119 | .school(school) | |
| 120 | .instructorEmail(currentUser.getUser().getEmail().strip()) | |
| 121 | .canvasApiToken(canvasApiToken) | |
| 122 | .canvasCourseId(canvasCourseId) | |
| 123 | .build(); | |
| 124 | Course savedCourse = courseRepository.save(course); | |
| 125 | ||
| 126 |
1
1. postCourse : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::postCourse → KILLED |
return new InstructorCourseView(savedCourse); |
| 127 | } | |
| 128 | ||
| 129 | /** Projection of Course entity with fields that are relevant for instructors and admins */ | |
| 130 | public static record InstructorCourseView( | |
| 131 | Long id, | |
| 132 | String courseName, | |
| 133 | String term, | |
| 134 | School school, | |
| 135 | String instructorEmail, | |
| 136 | int numStudents, | |
| 137 | int numStaff, | |
| 138 | Long plRepoId, | |
| 139 | Long plInstanceId, | |
| 140 | // Human-readable details of the PL associations; resolved only by the | |
| 141 | // single-course endpoints (getCourseById and the two update endpoints), | |
| 142 | // null in course lists. | |
| 143 | String plRepoName, | |
| 144 | String plInstanceShortName, | |
| 145 | Long plInstanceNumericId, | |
| 146 | int xSpacing, | |
| 147 | int ySpacing) { | |
| 148 | ||
| 149 | // Creates view from Course entity | |
| 150 | public InstructorCourseView(Course c) { | |
| 151 | this( | |
| 152 | c.getId(), | |
| 153 | c.getCourseName(), | |
| 154 | c.getTerm(), | |
| 155 | c.getSchool(), | |
| 156 | c.getInstructorEmail(), | |
| 157 |
1
1. <init> : negated conditional → KILLED |
c.getRosterStudents() != null ? c.getRosterStudents().size() : 0, |
| 158 |
1
1. <init> : negated conditional → KILLED |
c.getCourseStaff() != null ? c.getCourseStaff().size() : 0, |
| 159 | c.getPlRepoId(), | |
| 160 | c.getPlInstanceId(), | |
| 161 | null, | |
| 162 | null, | |
| 163 | null, | |
| 164 | c.getXSpacing(), | |
| 165 | c.getYSpacing()); | |
| 166 | } | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Builds an InstructorCourseView with the PL association details (repo name, instance short name, | |
| 171 | * instance numeric id) resolved from their tables — used by the single-course endpoints so the | |
| 172 | * frontend can show what is currently associated. List endpoints use the plain constructor and | |
| 173 | * leave these null. | |
| 174 | */ | |
| 175 | private InstructorCourseView viewWithPlDetails(Course c) { | |
| 176 | String plRepoName = | |
| 177 |
1
1. viewWithPlDetails : negated conditional → KILLED |
c.getPlRepoId() == null |
| 178 | ? null | |
| 179 | : plRepoRepository.findById(c.getPlRepoId()).map(PlRepo::getRepoName).orElse(null); | |
| 180 | PlInstance plInstance = | |
| 181 |
1
1. viewWithPlDetails : negated conditional → KILLED |
c.getPlInstanceId() == null |
| 182 | ? null | |
| 183 | : plInstanceRepository.findById(c.getPlInstanceId()).orElse(null); | |
| 184 |
1
1. viewWithPlDetails : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::viewWithPlDetails → KILLED |
return new InstructorCourseView( |
| 185 | c.getId(), | |
| 186 | c.getCourseName(), | |
| 187 | c.getTerm(), | |
| 188 | c.getSchool(), | |
| 189 | c.getInstructorEmail(), | |
| 190 |
1
1. viewWithPlDetails : negated conditional → KILLED |
c.getRosterStudents() != null ? c.getRosterStudents().size() : 0, |
| 191 |
1
1. viewWithPlDetails : negated conditional → KILLED |
c.getCourseStaff() != null ? c.getCourseStaff().size() : 0, |
| 192 | c.getPlRepoId(), | |
| 193 | c.getPlInstanceId(), | |
| 194 | plRepoName, | |
| 195 |
1
1. viewWithPlDetails : negated conditional → KILLED |
plInstance == null ? null : plInstance.getShortName(), |
| 196 |
1
1. viewWithPlDetails : negated conditional → KILLED |
plInstance == null ? null : plInstance.getNumericId(), |
| 197 | c.getXSpacing(), | |
| 198 | c.getYSpacing()); | |
| 199 | } | |
| 200 | ||
| 201 | /** | |
| 202 | * This method returns a list of courses. | |
| 203 | * | |
| 204 | * @return a list of all courses for an instructor. | |
| 205 | */ | |
| 206 | @Operation(summary = "List all courses for an instructor") | |
| 207 | @PreAuthorize("hasRole('ROLE_INSTRUCTOR')") | |
| 208 | @GetMapping("/list/instructors") | |
| 209 | public Iterable<InstructorCourseView> allForInstructors() { | |
| 210 | CurrentUser currentUser = getCurrentUser(); | |
| 211 | String instructorEmail = currentUser.getUser().getEmail(); | |
| 212 | List<Course> courses = courseRepository.findByInstructorEmail(instructorEmail); | |
| 213 | ||
| 214 | List<InstructorCourseView> courseViews = | |
| 215 | courses.stream().map(InstructorCourseView::new).collect(Collectors.toList()); | |
| 216 |
1
1. allForInstructors : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::allForInstructors → KILLED |
return courseViews; |
| 217 | } | |
| 218 | ||
| 219 | /** | |
| 220 | * This method returns a list of courses. | |
| 221 | * | |
| 222 | * @return a list of all courses for an admin. | |
| 223 | */ | |
| 224 | @Operation(summary = "List all courses for an admin") | |
| 225 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 226 | @GetMapping("/list/admins") | |
| 227 | public Iterable<InstructorCourseView> allForAdmins() { | |
| 228 | List<Course> courses = courseRepository.findAll(); | |
| 229 | ||
| 230 | List<InstructorCourseView> courseViews = | |
| 231 | courses.stream().map(InstructorCourseView::new).collect(Collectors.toList()); | |
| 232 |
1
1. allForAdmins : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::allForAdmins → KILLED |
return courseViews; |
| 233 | } | |
| 234 | ||
| 235 | /** | |
| 236 | * This method returns single course by its id | |
| 237 | * | |
| 238 | * @return a course | |
| 239 | */ | |
| 240 | @Operation(summary = "Get course by id") | |
| 241 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #id)") | |
| 242 | @GetMapping("/{id}") | |
| 243 | public InstructorCourseView getCourseById(@Parameter(name = "id") @PathVariable Long id) { | |
| 244 | Course course = | |
| 245 | courseRepository | |
| 246 | .findById(id) | |
| 247 |
1
1. lambda$getCourseById$0 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseById$0 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, id)); |
| 248 |
1
1. getCourseById : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseById → KILLED |
return viewWithPlDetails(course); |
| 249 | } | |
| 250 | ||
| 251 | /** | |
| 252 | * This method returns the Canvas course ID and partially obscured Canvas token for a course by | |
| 253 | * its id. If the token is less than or equal to 3 characters long, it is returned in full. | |
| 254 | * Otherwise, all but the last three characters are replaced with asterisks. This is okay because | |
| 255 | * such short tokens are not generated by Canvas. | |
| 256 | * | |
| 257 | * @param courseId the id of the course | |
| 258 | * @return a map with courseId, canvasCourseId, and obscured canvasApiToken | |
| 259 | */ | |
| 260 | @Operation(summary = "Get course Canvas course ID and Canvas token (partially obscured)") | |
| 261 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 262 | @GetMapping("getCanvasInfo") | |
| 263 | public Map<String, String> getCourseCanvasInfo( | |
| 264 | @Parameter(name = "courseId") @RequestParam Long courseId) { | |
| 265 | Course course = | |
| 266 | courseRepository | |
| 267 | .findById(courseId) | |
| 268 |
1
1. lambda$getCourseCanvasInfo$1 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseCanvasInfo$1 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 269 | ||
| 270 | String obscuredToken = null; | |
| 271 | ||
| 272 |
1
1. getCourseCanvasInfo : negated conditional → KILLED |
if (course.getCanvasApiToken() != null) { |
| 273 | String token = course.getCanvasApiToken(); | |
| 274 |
2
1. getCourseCanvasInfo : negated conditional → KILLED 2. getCourseCanvasInfo : changed conditional boundary → KILLED |
if (token.length() < 4) { |
| 275 | obscuredToken = token; | |
| 276 | } else { | |
| 277 |
1
1. getCourseCanvasInfo : Replaced integer subtraction with addition → KILLED |
String lastThree = token.substring(token.length() - 3); |
| 278 |
1
1. getCourseCanvasInfo : Replaced integer subtraction with addition → KILLED |
obscuredToken = "*".repeat(token.length() - 3) + lastThree; |
| 279 | } | |
| 280 | } | |
| 281 |
1
1. getCourseCanvasInfo : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseCanvasInfo → KILLED |
return Map.of( |
| 282 | "courseId", course.getId().toString(), | |
| 283 |
1
1. getCourseCanvasInfo : negated conditional → KILLED |
"canvasCourseId", course.getCanvasCourseId() != null ? course.getCanvasCourseId() : "", |
| 284 |
1
1. getCourseCanvasInfo : negated conditional → KILLED |
"canvasApiToken", obscuredToken != null ? obscuredToken : ""); |
| 285 | } | |
| 286 | ||
| 287 | public record RosterStudentCoursesDTO( | |
| 288 | Long id, String courseName, String term, String school, Long rosterStudentId) {} | |
| 289 | ||
| 290 | /** | |
| 291 | * This method returns a list of courses that the current user is enrolled. | |
| 292 | * | |
| 293 | * @return a list of courses in the DTO form along with the student status in the organization. | |
| 294 | */ | |
| 295 | @Operation(summary = "List all courses for the current student, including their org status") | |
| 296 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 297 | @GetMapping("/list/students") | |
| 298 | public List<RosterStudentCoursesDTO> listCoursesForCurrentUser() { | |
| 299 | String email = getCurrentUser().getUser().getEmail(); | |
| 300 | Iterable<RosterStudent> rosterStudentsIterable = rosterStudentRepository.findAllByEmail(email); | |
| 301 | List<RosterStudent> rosterStudents = new ArrayList<>(); | |
| 302 |
1
1. listCoursesForCurrentUser : removed call to java/lang/Iterable::forEach → KILLED |
rosterStudentsIterable.forEach(rosterStudents::add); |
| 303 |
1
1. listCoursesForCurrentUser : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::listCoursesForCurrentUser → KILLED |
return rosterStudents.stream() |
| 304 | .map( | |
| 305 | rs -> { | |
| 306 | Course course = rs.getCourse(); | |
| 307 | RosterStudentCoursesDTO rsDto = | |
| 308 | new RosterStudentCoursesDTO( | |
| 309 | course.getId(), | |
| 310 | course.getCourseName(), | |
| 311 | course.getTerm(), | |
| 312 | course.getSchool().getDisplayName(), | |
| 313 | rs.getId()); | |
| 314 |
1
1. lambda$listCoursesForCurrentUser$2 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUser$2 → KILLED |
return rsDto; |
| 315 | }) | |
| 316 | .collect(Collectors.toList()); | |
| 317 | } | |
| 318 | ||
| 319 | public record StaffCoursesDTO( | |
| 320 | Long id, String courseName, String term, School school, Long staffId) {} | |
| 321 | ||
| 322 | public enum EmailTypes { | |
| 323 | STUDENTS, | |
| 324 | STAFF, | |
| 325 | ALL | |
| 326 | } | |
| 327 | ||
| 328 | public enum EmailFormats { | |
| 329 | COMMA_SEPARATED, | |
| 330 | ONE_PER_LINE | |
| 331 | } | |
| 332 | ||
| 333 | /** | |
| 334 | * student see what courses they appear as staff in | |
| 335 | * | |
| 336 | * @param studentId the id of the student making request | |
| 337 | * @return a list of all courses student is staff in | |
| 338 | */ | |
| 339 | @Operation(summary = "Student see what courses they appear as staff in") | |
| 340 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 341 | @GetMapping("/list/staff") | |
| 342 | public List<StaffCoursesDTO> staffCourses() { | |
| 343 | CurrentUser currentUser = getCurrentUser(); | |
| 344 | User user = currentUser.getUser(); | |
| 345 | ||
| 346 | String email = user.getEmail(); | |
| 347 | ||
| 348 | List<CourseStaff> staffMembers = courseStaffRepository.findAllByEmail(email); | |
| 349 |
1
1. staffCourses : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::staffCourses → KILLED |
return staffMembers.stream() |
| 350 | .map( | |
| 351 | s -> { | |
| 352 | Course course = s.getCourse(); | |
| 353 | StaffCoursesDTO sDto = | |
| 354 | new StaffCoursesDTO( | |
| 355 | course.getId(), | |
| 356 | course.getCourseName(), | |
| 357 | course.getTerm(), | |
| 358 | course.getSchool(), | |
| 359 | s.getId()); | |
| 360 |
1
1. lambda$staffCourses$3 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$staffCourses$3 → KILLED |
return sDto; |
| 361 | }) | |
| 362 | .collect(Collectors.toList()); | |
| 363 | } | |
| 364 | ||
| 365 | /** DTO representing a course along with the ways in which the current user has access to it. */ | |
| 366 | public record CourseListDTO( | |
| 367 | Long id, | |
| 368 | String courseName, | |
| 369 | String term, | |
| 370 | School school, | |
| 371 | String instructorEmail, | |
| 372 | boolean studentAccess, | |
| 373 | boolean staffAccess, | |
| 374 | boolean instructorAccess, | |
| 375 | boolean adminAccess) {} | |
| 376 | ||
| 377 | /** | |
| 378 | * This method returns a unified list of courses that the current user has access to, whether as a | |
| 379 | * student, staff member, instructor, or admin. | |
| 380 | * | |
| 381 | * @return a list of courses along with the access flags for the current user. | |
| 382 | */ | |
| 383 | @Operation(summary = "List all courses the current user has access to") | |
| 384 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 385 | @GetMapping("/list") | |
| 386 | public List<CourseListDTO> listCoursesForCurrentUserUnified() { | |
| 387 | String email = getCurrentUser().getUser().getEmail(); | |
| 388 | ||
| 389 | boolean isAdmin = adminRepository.existsByEmail(email); | |
| 390 | ||
| 391 | Set<Long> studentCourseIds = | |
| 392 | rosterStudentRepository.findAllByEmail(email).stream() | |
| 393 |
1
1. lambda$listCoursesForCurrentUserUnified$4 : replaced Long return value with 0L for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$4 → KILLED |
.map(rs -> rs.getCourse().getId()) |
| 394 | .collect(Collectors.toSet()); | |
| 395 | ||
| 396 | Set<Long> staffCourseIds = | |
| 397 | courseStaffRepository.findAllByEmail(email).stream() | |
| 398 |
1
1. lambda$listCoursesForCurrentUserUnified$5 : replaced Long return value with 0L for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$5 → KILLED |
.map(cs -> cs.getCourse().getId()) |
| 399 | .collect(Collectors.toSet()); | |
| 400 | ||
| 401 | Set<Long> instructorCourseIds = | |
| 402 | courseRepository.findByInstructorEmail(email).stream() | |
| 403 | .map(Course::getId) | |
| 404 | .collect(Collectors.toSet()); | |
| 405 | ||
| 406 | List<Course> courses; | |
| 407 |
1
1. listCoursesForCurrentUserUnified : negated conditional → KILLED |
if (isAdmin) { |
| 408 | courses = courseRepository.findAll(); | |
| 409 | } else { | |
| 410 | Set<Long> accessibleCourseIds = new HashSet<>(); | |
| 411 | accessibleCourseIds.addAll(studentCourseIds); | |
| 412 | accessibleCourseIds.addAll(staffCourseIds); | |
| 413 | accessibleCourseIds.addAll(instructorCourseIds); | |
| 414 | courses = courseRepository.findAllById(accessibleCourseIds); | |
| 415 | } | |
| 416 | ||
| 417 |
1
1. listCoursesForCurrentUserUnified : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/CoursesController::listCoursesForCurrentUserUnified → KILLED |
return courses.stream() |
| 418 | .map( | |
| 419 | c -> | |
| 420 |
1
1. lambda$listCoursesForCurrentUserUnified$6 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$listCoursesForCurrentUserUnified$6 → KILLED |
new CourseListDTO( |
| 421 | c.getId(), | |
| 422 | c.getCourseName(), | |
| 423 | c.getTerm(), | |
| 424 | c.getSchool(), | |
| 425 | c.getInstructorEmail(), | |
| 426 | studentCourseIds.contains(c.getId()), | |
| 427 | staffCourseIds.contains(c.getId()), | |
| 428 | instructorCourseIds.contains(c.getId()), | |
| 429 | isAdmin)) | |
| 430 | .collect(Collectors.toList()); | |
| 431 | } | |
| 432 | ||
| 433 | /** | |
| 434 | * This method returns the unified access info for a single course, if the current user has access | |
| 435 | * to it. If the user does not have access, or the course does not exist, a 404 is returned. | |
| 436 | * | |
| 437 | * @param courseId the id of the course | |
| 438 | * @return the course along with the access flags for the current user. | |
| 439 | */ | |
| 440 | @Operation(summary = "Get unified course access info for a single course") | |
| 441 | @PreAuthorize("hasRole('ROLE_USER')") | |
| 442 | @GetMapping("/list/{courseId}") | |
| 443 | public CourseListDTO getCourseAccessInfo( | |
| 444 | @Parameter(name = "courseId") @PathVariable Long courseId) { | |
| 445 | String email = getCurrentUser().getUser().getEmail(); | |
| 446 | ||
| 447 | Course course = | |
| 448 | courseRepository | |
| 449 | .findById(courseId) | |
| 450 |
1
1. lambda$getCourseAccessInfo$7 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$7 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 451 | ||
| 452 | boolean isAdmin = adminRepository.existsByEmail(email); | |
| 453 | ||
| 454 | boolean studentAccess = | |
| 455 | rosterStudentRepository.findAllByEmail(email).stream() | |
| 456 |
2
1. lambda$getCourseAccessInfo$8 : replaced boolean return with false for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$8 → KILLED 2. lambda$getCourseAccessInfo$8 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$8 → KILLED |
.anyMatch(rs -> rs.getCourse().getId().equals(courseId)); |
| 457 | ||
| 458 | boolean staffAccess = | |
| 459 | courseStaffRepository.findAllByEmail(email).stream() | |
| 460 |
2
1. lambda$getCourseAccessInfo$9 : replaced boolean return with false for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$9 → KILLED 2. lambda$getCourseAccessInfo$9 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$getCourseAccessInfo$9 → KILLED |
.anyMatch(cs -> cs.getCourse().getId().equals(courseId)); |
| 461 | ||
| 462 | boolean instructorAccess = email.equals(course.getInstructorEmail()); | |
| 463 | ||
| 464 |
4
1. getCourseAccessInfo : negated conditional → KILLED 2. getCourseAccessInfo : negated conditional → KILLED 3. getCourseAccessInfo : negated conditional → KILLED 4. getCourseAccessInfo : negated conditional → KILLED |
if (!isAdmin && !studentAccess && !staffAccess && !instructorAccess) { |
| 465 | throw new EntityNotFoundException(Course.class, courseId); | |
| 466 | } | |
| 467 | ||
| 468 |
1
1. getCourseAccessInfo : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseAccessInfo → KILLED |
return new CourseListDTO( |
| 469 | course.getId(), | |
| 470 | course.getCourseName(), | |
| 471 | course.getTerm(), | |
| 472 | course.getSchool(), | |
| 473 | course.getInstructorEmail(), | |
| 474 | studentAccess, | |
| 475 | staffAccess, | |
| 476 | instructorAccess, | |
| 477 | isAdmin); | |
| 478 | } | |
| 479 | ||
| 480 | @Operation(summary = "Update instructor email for a course (admin only)") | |
| 481 | @PreAuthorize("hasRole('ROLE_ADMIN')") | |
| 482 | @PutMapping("/updateInstructor") | |
| 483 | public InstructorCourseView updateInstructorEmail( | |
| 484 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 485 | @Parameter(name = "instructorEmail") @RequestParam String instructorEmail) { | |
| 486 | ||
| 487 | instructorEmail = instructorEmail.strip(); | |
| 488 | ||
| 489 | Course course = | |
| 490 | courseRepository | |
| 491 | .findById(courseId) | |
| 492 |
1
1. lambda$updateInstructorEmail$10 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateInstructorEmail$10 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 493 | ||
| 494 | // Validate that the email exists in either instructor or admin table | |
| 495 | boolean isInstructor = instructorRepository.existsByEmail(instructorEmail); | |
| 496 | boolean isAdmin = adminRepository.existsByEmail(instructorEmail); | |
| 497 | ||
| 498 |
2
1. updateInstructorEmail : negated conditional → KILLED 2. updateInstructorEmail : negated conditional → KILLED |
if (!isInstructor && !isAdmin) { |
| 499 | throw new IllegalArgumentException("Email must belong to either an instructor or admin"); | |
| 500 | } | |
| 501 | ||
| 502 |
1
1. updateInstructorEmail : removed call to edu/ucsb/cs/scaffold/entity/Course::setInstructorEmail → KILLED |
course.setInstructorEmail(instructorEmail); |
| 503 | Course savedCourse = courseRepository.save(course); | |
| 504 | ||
| 505 |
1
1. updateInstructorEmail : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateInstructorEmail → KILLED |
return new InstructorCourseView(savedCourse); |
| 506 | } | |
| 507 | ||
| 508 | @Operation(summary = "Get course emails") | |
| 509 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 510 | @GetMapping("/emails") | |
| 511 | public String getCourseEmails( | |
| 512 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 513 | @Parameter(name = "type") @RequestParam(defaultValue = "STUDENTS") EmailTypes type, | |
| 514 | @Parameter(name = "format") @RequestParam(defaultValue = "ONE_PER_LINE") | |
| 515 | EmailFormats format) { | |
| 516 | ||
| 517 | List<String> staffEmails = | |
| 518 | StreamSupport.stream(courseStaffRepository.findByCourseId(courseId).spliterator(), false) | |
| 519 | .map(CourseStaff::getEmail) | |
| 520 | .filter(Objects::nonNull) | |
| 521 | .sorted() | |
| 522 | .collect(Collectors.toList()); | |
| 523 | ||
| 524 | List<String> studentEmails = | |
| 525 | StreamSupport.stream(rosterStudentRepository.findByCourseId(courseId).spliterator(), false) | |
| 526 | .map(RosterStudent::getEmail) | |
| 527 | .filter(Objects::nonNull) | |
| 528 | .sorted() | |
| 529 | .collect(Collectors.toList()); | |
| 530 | ||
| 531 | List<String> emails = studentEmails; | |
| 532 |
1
1. getCourseEmails : negated conditional → KILLED |
if (type == EmailTypes.STAFF) { |
| 533 | emails = staffEmails; | |
| 534 |
1
1. getCourseEmails : negated conditional → KILLED |
} else if (type == EmailTypes.ALL) { |
| 535 | emails = new ArrayList<>(staffEmails); | |
| 536 | emails.addAll(studentEmails); | |
| 537 | } | |
| 538 | ||
| 539 |
1
1. getCourseEmails : negated conditional → KILLED |
String separator = format == EmailFormats.COMMA_SEPARATED ? "," : "\r\n"; |
| 540 |
1
1. getCourseEmails : replaced return value with "" for edu/ucsb/cs/scaffold/controller/CoursesController::getCourseEmails → KILLED |
return String.join(separator, emails); |
| 541 | } | |
| 542 | ||
| 543 | @Operation(summary = "Delete a course") | |
| 544 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 545 | @DeleteMapping("") | |
| 546 | @Transactional | |
| 547 | public Object deleteCourse(@RequestParam Long courseId) | |
| 548 | throws NoSuchAlgorithmException, InvalidKeySpecException { | |
| 549 | Course course = | |
| 550 | courseRepository | |
| 551 | .findById(courseId) | |
| 552 |
1
1. lambda$deleteCourse$11 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$deleteCourse$11 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 553 | ||
| 554 | // Check if course has roster students or staff | |
| 555 |
2
1. deleteCourse : negated conditional → KILLED 2. deleteCourse : negated conditional → KILLED |
if (!course.getRosterStudents().isEmpty() || !course.getCourseStaff().isEmpty()) { |
| 556 | throw new IllegalArgumentException("Cannot delete course with students or staff"); | |
| 557 | } | |
| 558 | ||
| 559 |
1
1. deleteCourse : removed call to edu/ucsb/cs156/jobs/repositories/JobsRepository::deleteByScopeTypeAndScopeId → KILLED |
jobsRepository.deleteByScopeTypeAndScopeId("course", courseId); |
| 560 |
1
1. deleteCourse : removed call to edu/ucsb/cs/scaffold/repository/CourseRepository::delete → KILLED |
courseRepository.delete(course); |
| 561 |
1
1. deleteCourse : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::deleteCourse → KILLED |
return genericMessage("Course with id %s deleted".formatted(course.getId())); |
| 562 | } | |
| 563 | ||
| 564 | /** | |
| 565 | * This method updates an existing course. | |
| 566 | * | |
| 567 | * @param courseId the id of the course to update | |
| 568 | * @param courseName the new name of the course | |
| 569 | * @param term the new term of the course | |
| 570 | * @param school the new school of the course | |
| 571 | * @return the updated course | |
| 572 | */ | |
| 573 | @Operation(summary = "Update an existing course") | |
| 574 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 575 | @PutMapping("") | |
| 576 | public InstructorCourseView updateCourse( | |
| 577 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 578 | @Parameter(name = "courseName") @RequestParam String courseName, | |
| 579 | @Parameter(name = "term") @RequestParam String term, | |
| 580 | @Parameter(name = "school") @RequestParam School school) { | |
| 581 | Course course = | |
| 582 | courseRepository | |
| 583 | .findById(courseId) | |
| 584 |
1
1. lambda$updateCourse$12 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateCourse$12 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 585 | ||
| 586 |
1
1. updateCourse : removed call to edu/ucsb/cs/scaffold/entity/Course::setCourseName → KILLED |
course.setCourseName(courseName); |
| 587 |
1
1. updateCourse : removed call to edu/ucsb/cs/scaffold/entity/Course::setTerm → KILLED |
course.setTerm(term); |
| 588 |
1
1. updateCourse : removed call to edu/ucsb/cs/scaffold/entity/Course::setSchool → KILLED |
course.setSchool(school); |
| 589 | ||
| 590 | Course savedCourse = courseRepository.save(course); | |
| 591 | ||
| 592 |
1
1. updateCourse : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateCourse → KILLED |
return new InstructorCourseView(savedCourse); |
| 593 | } | |
| 594 | ||
| 595 | /** | |
| 596 | * This method updates an existing course. | |
| 597 | * | |
| 598 | * @param courseId the id of the course to update | |
| 599 | * @param courseName the new name of the course | |
| 600 | * @param term the new term of the course | |
| 601 | * @param school the new school of the course | |
| 602 | * @param canvasApiToken the new Canvas API token for the course | |
| 603 | * @param canvasCourseId the new Canvas course ID | |
| 604 | * @return the updated course | |
| 605 | */ | |
| 606 | @Operation(summary = "Update an existing course with Canvas token and course ID") | |
| 607 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 608 | @PutMapping("/updateCourseCanvasToken") | |
| 609 | public InstructorCourseView updateCourseWithCanvasToken( | |
| 610 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 611 | @Parameter(name = "canvasApiToken") @RequestParam(required = false) String canvasApiToken, | |
| 612 | @Parameter(name = "canvasCourseId") @RequestParam(required = false) String canvasCourseId) { | |
| 613 | Course course = | |
| 614 | courseRepository | |
| 615 | .findById(courseId) | |
| 616 |
1
1. lambda$updateCourseWithCanvasToken$13 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateCourseWithCanvasToken$13 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 617 | ||
| 618 |
1
1. updateCourseWithCanvasToken : negated conditional → KILLED |
if (canvasApiToken != null |
| 619 |
1
1. updateCourseWithCanvasToken : negated conditional → KILLED |
&& !canvasApiToken.isEmpty() |
| 620 |
1
1. updateCourseWithCanvasToken : negated conditional → KILLED |
&& !canvasApiToken.equals(course.getCanvasApiToken())) { |
| 621 |
1
1. updateCourseWithCanvasToken : removed call to edu/ucsb/cs/scaffold/entity/Course::setCanvasApiToken → KILLED |
course.setCanvasApiToken(canvasApiToken); |
| 622 | } | |
| 623 | ||
| 624 |
1
1. updateCourseWithCanvasToken : negated conditional → KILLED |
if (canvasCourseId != null |
| 625 |
1
1. updateCourseWithCanvasToken : negated conditional → KILLED |
&& !canvasCourseId.isEmpty() |
| 626 |
1
1. updateCourseWithCanvasToken : negated conditional → KILLED |
&& !canvasCourseId.equals(course.getCanvasCourseId())) { |
| 627 |
1
1. updateCourseWithCanvasToken : removed call to edu/ucsb/cs/scaffold/entity/Course::setCanvasCourseId → KILLED |
course.setCanvasCourseId(canvasCourseId); |
| 628 | } | |
| 629 | ||
| 630 | Course savedCourse = courseRepository.save(course); | |
| 631 | ||
| 632 |
1
1. updateCourseWithCanvasToken : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateCourseWithCanvasToken → KILLED |
return new InstructorCourseView(savedCourse); |
| 633 | } | |
| 634 | ||
| 635 | /** | |
| 636 | * Associates a GitHub repo (PlRepo) with a course, after verifying that the current user's stored | |
| 637 | * GitHub PAT has read/write access to the repo. The check is a single GET /repos/{owner}/{repo} | |
| 638 | * call: a successful response proves read access and its permissions block reports push (write) | |
| 639 | * access, so nothing is written to the repo. | |
| 640 | * | |
| 641 | * @param courseId the id of the course | |
| 642 | * @param repoName the repo in owner/repo form (i.e. the part after https://github.com/) | |
| 643 | * @return the updated course | |
| 644 | */ | |
| 645 | @Operation(summary = "Associate a GitHub repo (PlRepo) with a course") | |
| 646 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 647 | @PutMapping("/updateGithubRepo") | |
| 648 | public InstructorCourseView updateGithubRepo( | |
| 649 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 650 | @Parameter(name = "repoName", description = "GitHub repo in owner/repo form") @RequestParam | |
| 651 | String repoName) { | |
| 652 | Course course = | |
| 653 | courseRepository | |
| 654 | .findById(courseId) | |
| 655 |
1
1. lambda$updateGithubRepo$14 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$14 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 656 | ||
| 657 | long userId = getCurrentUser().getUser().getId(); | |
| 658 | PatCredential credential = | |
| 659 | patCredentialRepository | |
| 660 | .findByUserIdAndPlatform(userId, PatPlatform.GITHUB) | |
| 661 |
1
1. lambda$updateGithubRepo$15 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$15 → KILLED |
.orElseThrow(() -> new ForbiddenException("must set up Github PAT first")); |
| 662 | String token = | |
| 663 | patEncryptionService.decrypt(credential.getCiphertext(), credential.getKeyVersion()); | |
| 664 | ||
| 665 | String trimmedRepoName = repoName.strip(); | |
| 666 | boolean canWrite; | |
| 667 | try { | |
| 668 | canWrite = githubService.hasWriteAccess(trimmedRepoName, token); | |
| 669 | } catch (HttpClientErrorException e) { | |
| 670 | throw new ForbiddenException("No access to repo via Github PAT token"); | |
| 671 | } | |
| 672 |
1
1. updateGithubRepo : negated conditional → KILLED |
if (!canWrite) { |
| 673 | throw new ForbiddenException("Read/write access to repo via Github PAT is required"); | |
| 674 | } | |
| 675 | ||
| 676 | PlRepo plRepo = | |
| 677 | plRepoRepository | |
| 678 | .findByRepoName(trimmedRepoName) | |
| 679 | .orElseGet( | |
| 680 |
1
1. lambda$updateGithubRepo$16 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updateGithubRepo$16 → KILLED |
() -> plRepoRepository.save(PlRepo.builder().repoName(trimmedRepoName).build())); |
| 681 |
1
1. updateGithubRepo : removed call to edu/ucsb/cs/scaffold/entity/Course::setPlRepoId → KILLED |
course.setPlRepoId(plRepo.getId()); |
| 682 |
1
1. updateGithubRepo : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updateGithubRepo → KILLED |
return viewWithPlDetails(courseRepository.save(course)); |
| 683 | } | |
| 684 | ||
| 685 | /** | |
| 686 | * Associates a PrairieLearn course instance (PlInstance) with a course. The numeric instance id | |
| 687 | * is verified in two steps: it is fetched from the PrairieLearn API using the caller's | |
| 688 | * PrairieLearn PAT, and the course's GitHub repo must contain a matching | |
| 689 | * courseInstances/{shortName}/infoCourseInstance.json whose longName agrees. Only the | |
| 690 | * PrairieLearn API can supply the numeric id, and only the repo check proves the instance belongs | |
| 691 | * to this course's repo. | |
| 692 | * | |
| 693 | * @param courseId the id of the course | |
| 694 | * @param instanceId PrairieLearn's numeric course instance id | |
| 695 | * @return the updated course | |
| 696 | */ | |
| 697 | @Operation(summary = "Associate a PrairieLearn course instance (PlInstance) with a course") | |
| 698 | @PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)") | |
| 699 | @PutMapping("/updatePLInstance") | |
| 700 | public InstructorCourseView updatePLInstance( | |
| 701 | @Parameter(name = "courseId") @RequestParam Long courseId, | |
| 702 | @Parameter(name = "instanceId", description = "numeric PrairieLearn course instance id") | |
| 703 | @RequestParam | |
| 704 | Long instanceId) { | |
| 705 | Course course = | |
| 706 | courseRepository | |
| 707 | .findById(courseId) | |
| 708 |
1
1. lambda$updatePLInstance$17 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$17 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(Course.class, courseId)); |
| 709 | ||
| 710 | long userId = getCurrentUser().getUser().getId(); | |
| 711 | PatCredential githubCredential = | |
| 712 | patCredentialRepository | |
| 713 | .findByUserIdAndPlatform(userId, PatPlatform.GITHUB) | |
| 714 |
1
1. lambda$updatePLInstance$18 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$18 → KILLED |
.orElseThrow(() -> new ForbiddenException("must set up Github PAT first")); |
| 715 | PatCredential plCredential = | |
| 716 | patCredentialRepository | |
| 717 | .findByUserIdAndPlatform(userId, PatPlatform.PRAIRIELEARN) | |
| 718 |
1
1. lambda$updatePLInstance$19 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$19 → KILLED |
.orElseThrow(() -> new ForbiddenException("must set up PrairieLearn PAT first")); |
| 719 |
1
1. updatePLInstance : negated conditional → KILLED |
if (course.getPlRepoId() == null) { |
| 720 | throw new ForbiddenException("must associate course with PlRepo first"); | |
| 721 | } | |
| 722 | PlRepo plRepo = | |
| 723 | plRepoRepository | |
| 724 | .findById(course.getPlRepoId()) | |
| 725 |
1
1. lambda$updatePLInstance$20 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$20 → KILLED |
.orElseThrow(() -> new EntityNotFoundException(PlRepo.class, course.getPlRepoId())); |
| 726 | ||
| 727 | String plToken = | |
| 728 | patEncryptionService.decrypt(plCredential.getCiphertext(), plCredential.getKeyVersion()); | |
| 729 | PrairieLearnService.CourseInstanceInfo info; | |
| 730 | try { | |
| 731 | info = prairieLearnService.getCourseInstance(instanceId, plToken); | |
| 732 | } catch (HttpClientErrorException e) { | |
| 733 | throw new ForbiddenException("course instance id not found"); | |
| 734 | } | |
| 735 |
1
1. updatePLInstance : negated conditional → KILLED |
if (info == null) { |
| 736 | throw new ForbiddenException("course instance id not found"); | |
| 737 | } | |
| 738 | ||
| 739 | String githubToken = | |
| 740 | patEncryptionService.decrypt( | |
| 741 | githubCredential.getCiphertext(), githubCredential.getKeyVersion()); | |
| 742 |
1
1. updatePLInstance : negated conditional → KILLED |
if (!repoConfirmsInstance(plRepo, info, githubToken)) { |
| 743 | throw new ForbiddenException("course instance id not found"); | |
| 744 | } | |
| 745 | ||
| 746 | PlInstance plInstance = | |
| 747 | plInstanceRepository | |
| 748 | .findByPlRepoIdAndShortName(plRepo.getId(), info.shortName()) | |
| 749 | .orElseGet( | |
| 750 | () -> | |
| 751 |
1
1. lambda$updatePLInstance$21 : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::lambda$updatePLInstance$21 → KILLED |
PlInstance.builder() |
| 752 | .plRepoId(plRepo.getId()) | |
| 753 | .shortName(info.shortName()) | |
| 754 | .build()); | |
| 755 |
1
1. updatePLInstance : removed call to edu/ucsb/cs/scaffold/entity/PlInstance::setLongName → KILLED |
plInstance.setLongName(info.longName()); |
| 756 |
1
1. updatePLInstance : removed call to edu/ucsb/cs/scaffold/entity/PlInstance::setNumericId → KILLED |
plInstance.setNumericId(info.courseInstanceId()); |
| 757 | PlInstance savedInstance = plInstanceRepository.save(plInstance); | |
| 758 | ||
| 759 |
1
1. updatePLInstance : removed call to edu/ucsb/cs/scaffold/entity/Course::setPlInstanceId → KILLED |
course.setPlInstanceId(savedInstance.getId()); |
| 760 | Course savedCourse = courseRepository.save(course); | |
| 761 | ||
| 762 | // A successful association immediately kicks off a sync of the course's questions and | |
| 763 | // assessments (issue #69), so the instructor doesn't have to launch it by hand. | |
| 764 | jobService.runAsJob(syncCourseWithPlRepoJobFactory.create(userId, savedCourse)); | |
| 765 | ||
| 766 |
1
1. updatePLInstance : replaced return value with null for edu/ucsb/cs/scaffold/controller/CoursesController::updatePLInstance → KILLED |
return viewWithPlDetails(savedCourse); |
| 767 | } | |
| 768 | ||
| 769 | /** | |
| 770 | * True when the course's repo has courseInstances/{shortName}/infoCourseInstance.json and its | |
| 771 | * longName matches the one PrairieLearn reported for the instance. | |
| 772 | */ | |
| 773 | private boolean repoConfirmsInstance( | |
| 774 | PlRepo plRepo, PrairieLearnService.CourseInstanceInfo info, String githubToken) { | |
| 775 | String infoJson; | |
| 776 | try { | |
| 777 | infoJson = | |
| 778 | githubService.getFileContent( | |
| 779 | plRepo.getRepoName(), | |
| 780 | "courseInstances/" + info.shortName() + "/infoCourseInstance.json", | |
| 781 | githubToken); | |
| 782 | } catch (HttpClientErrorException e) { | |
| 783 |
1
1. repoConfirmsInstance : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED |
return false; |
| 784 | } | |
| 785 | try { | |
| 786 | JsonNode longName = new ObjectMapper().readTree(infoJson).path("longName"); | |
| 787 |
3
1. repoConfirmsInstance : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED 2. repoConfirmsInstance : negated conditional → KILLED 3. repoConfirmsInstance : negated conditional → KILLED |
return !longName.isMissingNode() && longName.asText().equals(info.longName()); |
| 788 | } catch (com.fasterxml.jackson.core.JsonProcessingException e) { | |
| 789 |
1
1. repoConfirmsInstance : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/CoursesController::repoConfirmsInstance → KILLED |
return false; |
| 790 | } | |
| 791 | } | |
| 792 | } | |
Mutations | ||
| 126 |
1.1 |
|
| 157 |
1.1 |
|
| 158 |
1.1 |
|
| 177 |
1.1 |
|
| 181 |
1.1 |
|
| 184 |
1.1 |
|
| 190 |
1.1 |
|
| 191 |
1.1 |
|
| 195 |
1.1 |
|
| 196 |
1.1 |
|
| 216 |
1.1 |
|
| 232 |
1.1 |
|
| 247 |
1.1 |
|
| 248 |
1.1 |
|
| 268 |
1.1 |
|
| 272 |
1.1 |
|
| 274 |
1.1 2.2 |
|
| 277 |
1.1 |
|
| 278 |
1.1 |
|
| 281 |
1.1 |
|
| 283 |
1.1 |
|
| 284 |
1.1 |
|
| 302 |
1.1 |
|
| 303 |
1.1 |
|
| 314 |
1.1 |
|
| 349 |
1.1 |
|
| 360 |
1.1 |
|
| 393 |
1.1 |
|
| 398 |
1.1 |
|
| 407 |
1.1 |
|
| 417 |
1.1 |
|
| 420 |
1.1 |
|
| 450 |
1.1 |
|
| 456 |
1.1 2.2 |
|
| 460 |
1.1 2.2 |
|
| 464 |
1.1 2.2 3.3 4.4 |
|
| 468 |
1.1 |
|
| 492 |
1.1 |
|
| 498 |
1.1 2.2 |
|
| 502 |
1.1 |
|
| 505 |
1.1 |
|
| 532 |
1.1 |
|
| 534 |
1.1 |
|
| 539 |
1.1 |
|
| 540 |
1.1 |
|
| 552 |
1.1 |
|
| 555 |
1.1 2.2 |
|
| 559 |
1.1 |
|
| 560 |
1.1 |
|
| 561 |
1.1 |
|
| 584 |
1.1 |
|
| 586 |
1.1 |
|
| 587 |
1.1 |
|
| 588 |
1.1 |
|
| 592 |
1.1 |
|
| 616 |
1.1 |
|
| 618 |
1.1 |
|
| 619 |
1.1 |
|
| 620 |
1.1 |
|
| 621 |
1.1 |
|
| 624 |
1.1 |
|
| 625 |
1.1 |
|
| 626 |
1.1 |
|
| 627 |
1.1 |
|
| 632 |
1.1 |
|
| 655 |
1.1 |
|
| 661 |
1.1 |
|
| 672 |
1.1 |
|
| 680 |
1.1 |
|
| 681 |
1.1 |
|
| 682 |
1.1 |
|
| 708 |
1.1 |
|
| 714 |
1.1 |
|
| 718 |
1.1 |
|
| 719 |
1.1 |
|
| 725 |
1.1 |
|
| 735 |
1.1 |
|
| 742 |
1.1 |
|
| 751 |
1.1 |
|
| 755 |
1.1 |
|
| 756 |
1.1 |
|
| 759 |
1.1 |
|
| 766 |
1.1 |
|
| 783 |
1.1 |
|
| 787 |
1.1 2.2 3.3 |
|
| 789 |
1.1 |