CopyConceptGraphJob.java

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
Location : getScopeType
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:scope_type_and_id_are_course_and_toCourseId()]
replaced return value with "" for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::getScopeType → KILLED

52

1.1
Location : getScopeId
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:scope_type_and_id_are_course_and_toCourseId()]
replaced Long return value with 0L for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::getScopeId → KILLED

57

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:admin_can_copy_concept_graph_between_courses_they_do_not_teach()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

63

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_both_courses_are_missing()]
negated conditional → KILLED

66

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_both_courses_are_missing()]
negated conditional → KILLED

69

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_both_courses_are_missing()]
negated conditional → KILLED

80

1.1
Location : lambda$accept$0
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_is_missing()]
replaced return value with null for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::lambda$accept$0 → KILLED

83

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_lacks_permission_on_from_course()]
negated conditional → KILLED

89

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_lacks_permission_on_to_course()]
negated conditional → KILLED

101

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_yaml_import_reports_failure()]
negated conditional → KILLED

105

1.1
Location : accept
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:admin_can_copy_concept_graph_between_courses_they_do_not_teach()]
removed call to edu/ucsb/cs156/jobs/services/JobContext::log → KILLED

118

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_lacks_permission_on_from_course()]
negated conditional → KILLED

119

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_yaml_import_reports_failure()]
replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED

121

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_lacks_permission_on_from_course()]
negated conditional → KILLED

122

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_is_not_a_matching_staff_member_on_from_course()]
negated conditional → KILLED

2.2
Location : lambda$hasManagePermissions$1
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_is_not_a_matching_staff_member_on_from_course()]
replaced boolean return with true for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::lambda$hasManagePermissions$1 → KILLED

3.3
Location : lambda$hasManagePermissions$1
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:staff_of_both_courses_can_copy_concept_graph()]
replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::lambda$hasManagePermissions$1 → KILLED

123

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:staff_of_both_courses_can_copy_concept_graph()]
replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED

125

1.1
Location : hasManagePermissions
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_lacks_permission_on_from_course()]
replaced boolean return with true for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED

2.2
Location : hasManagePermissions
Killed by : edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.jobs.CopyConceptGraphJobTests]/[method:terminates_when_user_lacks_permission_on_to_course()]
replaced boolean return with false for edu/ucsb/cs/scaffold/jobs/CopyConceptGraphJob::hasManagePermissions → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0