| 1 | package edu.ucsb.cs.scaffold.jobs; | |
| 2 | ||
| 3 | import edu.ucsb.cs.scaffold.entity.Course; | |
| 4 | import edu.ucsb.cs.scaffold.entity.User; | |
| 5 | import edu.ucsb.cs.scaffold.repository.AdminRepository; | |
| 6 | import edu.ucsb.cs.scaffold.repository.CourseRepository; | |
| 7 | import edu.ucsb.cs.scaffold.repository.UserRepository; | |
| 8 | import edu.ucsb.cs.scaffold.services.ConceptYamlService; | |
| 9 | import edu.ucsb.cs156.jobs.services.JobContext; | |
| 10 | import edu.ucsb.cs156.jobs.services.JobContextConsumer; | |
| 11 | import java.io.ByteArrayInputStream; | |
| 12 | import java.nio.charset.StandardCharsets; | |
| 13 | import java.util.ArrayList; | |
| 14 | import java.util.List; | |
| 15 | import java.util.Map; | |
| 16 | import java.util.Optional; | |
| 17 | import lombok.Builder; | |
| 18 | ||
| 19 | /** | |
| 20 | * Copies one course's entire concept-graph content (concepts, subconcepts, prerequisite edges, and | |
| 21 | * practice problems) into another course, replacing ALL of the destination course's content and | |
| 22 | * clearing every user's saved per-course scaffold state for that course. | |
| 23 | * | |
| 24 | * <p>Implemented as an in-memory YAML export of the "from" course immediately followed by a YAML | |
| 25 | * import into the "to" course (see docs/yaml-format.md), so this job cannot drift from the behavior | |
| 26 | * of the download/upload endpoints in {@link | |
| 27 | * edu.ucsb.cs.scaffold.controller.ConceptsYamlController}: the same code performs both, and any | |
| 28 | * future bug fix in {@link ConceptYamlService} applies to this job automatically. | |
| 29 | * | |
| 30 | * <p>Before copying anything, the job verifies that both courses exist and that the launching user | |
| 31 | * has admin or instructor/staff level access to both; otherwise it logs an error and terminates | |
| 32 | * without touching either course. | |
| 33 | */ | |
| 34 | @Builder | |
| 35 | public class CopyConceptGraphJob implements JobContextConsumer { | |
| 36 | ||
| 37 | private long userId; | |
| 38 | private Long fromCourseId; | |
| 39 | private Long toCourseId; | |
| 40 | private UserRepository userRepository; | |
| 41 | private CourseRepository courseRepository; | |
| 42 | private AdminRepository adminRepository; | |
| 43 | private ConceptYamlService conceptYamlService; | |
| 44 | ||
| 45 | @Override | |
| 46 | public String getScopeType() { | |
| 47 |
1
1. getScopeType : replaced return value with "" for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::getScopeType → KILLED |
return "course"; |
| 48 | } | |
| 49 | ||
| 50 | @Override | |
| 51 | public Long getScopeId() { | |
| 52 |
1
1. getScopeId : replaced Long return value with 0L for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::getScopeId → KILLED |
return toCourseId; |
| 53 | } | |
| 54 | ||
| 55 | @Override | |
| 56 | public void accept(JobContext ctx) throws Exception { | |
| 57 |
1
1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED |
ctx.log( |
| 58 | "Copying concept graph from course %d to course %d".formatted(fromCourseId, toCourseId)); | |
| 59 | ||
| 60 | Optional<Course> fromCourse = courseRepository.findById(fromCourseId); | |
| 61 | Optional<Course> toCourse = courseRepository.findById(toCourseId); | |
| 62 | List<String> missingCourses = new ArrayList<>(); | |
| 63 |
1
1. accept : negated conditional → KILLED |
if (fromCourse.isEmpty()) { |
| 64 | missingCourses.add("from course id %d".formatted(fromCourseId)); | |
| 65 | } | |
| 66 |
1
1. accept : negated conditional → KILLED |
if (toCourse.isEmpty()) { |
| 67 | missingCourses.add("to course id %d".formatted(toCourseId)); | |
| 68 | } | |
| 69 |
1
1. accept : negated conditional → KILLED |
if (!missingCourses.isEmpty()) { |
| 70 | throw new Exception( | |
| 71 | "Cannot copy concept graph: %s not found" | |
| 72 | .formatted(String.join(" and ", missingCourses))); | |
| 73 | } | |
| 74 | ||
| 75 | User user = | |
| 76 | userRepository | |
| 77 | .findById(userId) | |
| 78 | .orElseThrow( | |
| 79 | () -> | |
| 80 |
1
1. lambda$accept$0 : replaced return value with null for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::lambda$accept$0 → KILLED |
new Exception( |
| 81 | "Cannot copy concept graph: user %d not found".formatted(userId))); | |
| 82 | ||
| 83 |
1
1. accept : negated conditional → KILLED |
if (!hasManagePermissions(user, fromCourse.get())) { |
| 84 | throw new Exception( | |
| 85 | ("Cannot copy concept graph: user %s does not have admin or instructor/staff access" | |
| 86 | + " to the from course (id %d)") | |
| 87 | .formatted(user.getEmail(), fromCourseId)); | |
| 88 | } | |
| 89 |
1
1. accept : negated conditional → KILLED |
if (!hasManagePermissions(user, toCourse.get())) { |
| 90 | throw new Exception( | |
| 91 | ("Cannot copy concept graph: user %s does not have admin or instructor/staff access" | |
| 92 | + " to the to course (id %d)") | |
| 93 | .formatted(user.getEmail(), toCourseId)); | |
| 94 | } | |
| 95 | ||
| 96 | String yaml = conceptYamlService.createYAML(fromCourseId); | |
| 97 | Map<String, Object> report = | |
| 98 | conceptYamlService.replaceFromYAML( | |
| 99 | toCourseId, new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); | |
| 100 | ||
| 101 |
1
1. accept : negated conditional → KILLED |
if (!Boolean.TRUE.equals(report.get("success"))) { |
| 102 | throw new Exception("Failed to copy concept graph: %s".formatted(report.get("errors"))); | |
| 103 | } | |
| 104 | ||
| 105 |
1
1. accept : removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED |
ctx.log( |
| 106 | ("Copied concept graph: %s concepts, %s subconcepts, %s edges, %s practice problems" | |
| 107 | + " created; %s user states cleared") | |
| 108 | .formatted( | |
| 109 | report.get("conceptsCreated"), | |
| 110 | report.get("subconceptsCreated"), | |
| 111 | report.get("edgesCreated"), | |
| 112 | report.get("practiceProblemsCreated"), | |
| 113 | report.get("userStatesCleared"))); | |
| 114 | } | |
| 115 | ||
| 116 | private boolean hasManagePermissions(User user, Course course) { | |
| 117 | String email = user.getEmail(); | |
| 118 |
1
1. hasManagePermissions : negated conditional → KILLED |
if (adminRepository.existsByEmail(email)) { |
| 119 |
1
1. hasManagePermissions : replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED |
return true; |
| 120 | } | |
| 121 |
1
1. hasManagePermissions : negated conditional → KILLED |
if (course.getCourseStaff() != null |
| 122 |
3
1. hasManagePermissions : negated conditional → KILLED 2. lambda$hasManagePermissions$1 : replaced boolean return with true for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::lambda$hasManagePermissions$1 → KILLED 3. lambda$hasManagePermissions$1 : replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::lambda$hasManagePermissions$1 → KILLED |
&& course.getCourseStaff().stream().anyMatch(staff -> email.equals(staff.getEmail()))) { |
| 123 |
1
1. hasManagePermissions : replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED |
return true; |
| 124 | } | |
| 125 |
2
1. hasManagePermissions : replaced boolean return with true for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED 2. hasManagePermissions : replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED |
return email.equals(course.getInstructorEmail()); |
| 126 | } | |
| 127 | } | |
Mutations | ||
| 47 |
1.1 |
|
| 52 |
1.1 |
|
| 57 |
1.1 |
|
| 63 |
1.1 |
|
| 66 |
1.1 |
|
| 69 |
1.1 |
|
| 80 |
1.1 |
|
| 83 |
1.1 |
|
| 89 |
1.1 |
|
| 101 |
1.1 |
|
| 105 |
1.1 |
|
| 118 |
1.1 |
|
| 119 |
1.1 |
|
| 121 |
1.1 |
|
| 122 |
1.1 2.2 3.3 |
|
| 123 |
1.1 |
|
| 125 |
1.1 2.2 |