AssessmentController.java

1
package edu.ucsb.cs.scaffold.controller;
2
3
import com.fasterxml.jackson.annotation.JsonProperty;
4
import edu.ucsb.cs.scaffold.entity.Course;
5
import edu.ucsb.cs.scaffold.entity.PlAssessment;
6
import edu.ucsb.cs.scaffold.entity.PlAssessmentQuestion;
7
import edu.ucsb.cs.scaffold.entity.PlQuestion;
8
import edu.ucsb.cs.scaffold.errors.EntityNotFoundException;
9
import edu.ucsb.cs.scaffold.repository.CourseRepository;
10
import edu.ucsb.cs.scaffold.repository.PlAssessmentQuestionRepository;
11
import edu.ucsb.cs.scaffold.repository.PlAssessmentRepository;
12
import edu.ucsb.cs.scaffold.repository.PlQuestionRepository;
13
import io.swagger.v3.oas.annotations.Operation;
14
import io.swagger.v3.oas.annotations.Parameter;
15
import io.swagger.v3.oas.annotations.tags.Tag;
16
import java.util.Comparator;
17
import java.util.List;
18
import java.util.Map;
19
import java.util.Objects;
20
import java.util.stream.Collectors;
21
import lombok.RequiredArgsConstructor;
22
import org.springframework.security.access.prepost.PreAuthorize;
23
import org.springframework.web.bind.annotation.GetMapping;
24
import org.springframework.web.bind.annotation.PathVariable;
25
import org.springframework.web.bind.annotation.PutMapping;
26
import org.springframework.web.bind.annotation.RequestParam;
27
import org.springframework.web.bind.annotation.RestController;
28
29
// Serves ConceptGraphPage only (via useBackend, not legacyClient.ts). Assessments/questions come
30
// from the PrairieLearn-synced pl_assessment / pl_assessment_question / pl_question tables,
31
// scoped to the course's associated PlRepo + PlInstance, instead of the old hand-seeded
32
// assessments/questions tables (which now serve only LegacyHomePage via
33
// LegacyAssessmentController at /api/legacy/assessments).
34
@Tag(name = "Assessments")
35
@RestController
36
@RequiredArgsConstructor
37
public class AssessmentController extends ApiController {
38
39
  private final CourseRepository courseRepository;
40
  private final PlAssessmentRepository plAssessmentRepository;
41
  private final PlAssessmentQuestionRepository plAssessmentQuestionRepository;
42
  private final PlQuestionRepository plQuestionRepository;
43
44
  @Operation(
45
      summary =
46
          "List the unlocked assessments available for a course's associated PrairieLearn"
47
              + " repo/instance")
48
  @GetMapping("/api/assessments")
49
  public List<AssessmentDTO> getAssessments(
50
      @Parameter(description = "Id of the course") @RequestParam Long courseId) {
51
    Course course = courseRepository.findById(courseId).orElse(null);
52 3 1. getAssessments : negated conditional → KILLED
2. getAssessments : negated conditional → KILLED
3. getAssessments : negated conditional → KILLED
    if (course == null || course.getPlRepoId() == null || course.getPlInstanceId() == null) {
53
      // No PrairieLearn repo/instance associated with this course yet (or the course doesn't
54
      // exist): the menu shows no assessments rather than erroring.
55
      return List.of();
56
    }
57
58 1 1. getAssessments : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/AssessmentController::getAssessments → KILLED
    return plAssessmentRepository
59
        .findByPlRepoIdAndPlInstanceId(course.getPlRepoId(), course.getPlInstanceId())
60
        .stream()
61 2 1. lambda$getAssessments$0 : negated conditional → KILLED
2. lambda$getAssessments$0 : replaced boolean return with true for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getAssessments$0 → KILLED
        .filter(a -> !a.isLocked())
62
        .sorted(
63
            Comparator.comparing(
64
                    PlAssessment::getPlAssessmentOrder,
65
                    Comparator.nullsLast(Comparator.naturalOrder()))
66
                .thenComparing(PlAssessment::getName))
67
        .map(AssessmentController::toAssessmentDTO)
68
        .toList();
69
  }
70
71
  @Operation(
72
      summary =
73
          "List all assessments (locked and unlocked) for a course, for instructor management")
74
  @PreAuthorize("@CourseSecurity.hasInstructorPermissions(#root, #courseId)")
75
  @GetMapping("/api/assessments/all")
76
  public List<AssessmentManagementDTO> getAllAssessments(
77
      @Parameter(description = "Id of the course") @RequestParam Long courseId) {
78
    Course course =
79
        courseRepository
80
            .findById(courseId)
81 1 1. lambda$getAllAssessments$1 : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getAllAssessments$1 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
82 2 1. getAllAssessments : negated conditional → KILLED
2. getAllAssessments : negated conditional → KILLED
    if (course.getPlRepoId() == null || course.getPlInstanceId() == null) {
83
      // No PrairieLearn repo/instance associated with this course yet: a normal,
84
      // not-yet-configured state, not an error. The modal shows no assessments to manage.
85
      return List.of();
86
    }
87
88 1 1. getAllAssessments : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/AssessmentController::getAllAssessments → KILLED
    return plAssessmentRepository
89
        .findByPlRepoIdAndPlInstanceId(course.getPlRepoId(), course.getPlInstanceId())
90
        .stream()
91
        .sorted(
92
            Comparator.comparing(
93
                    PlAssessment::getPlAssessmentOrder,
94
                    Comparator.nullsLast(Comparator.naturalOrder()))
95
                .thenComparing(PlAssessment::getName))
96
        .map(AssessmentController::toAssessmentManagementDTO)
97
        .toList();
98
  }
99
100
  @Operation(summary = "Lock or unlock an assessment so it is hidden from or shown to students")
101
  @PreAuthorize("@CourseSecurity.hasInstructorPermissions(#root, #courseId)")
102
  @PutMapping("/api/assessments/lock")
103
  public AssessmentManagementDTO setLocked(
104
      @Parameter(description = "Id of the course") @RequestParam Long courseId,
105
      @Parameter(description = "Id of the PlAssessment") @RequestParam Long assessmentId,
106
      @Parameter(description = "true to lock (hide), false to unlock (show)") @RequestParam
107
          boolean locked) {
108
    Course course =
109
        courseRepository
110
            .findById(courseId)
111 1 1. lambda$setLocked$2 : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$setLocked$2 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(Course.class, courseId));
112
    PlAssessment assessment =
113
        plAssessmentRepository
114
            .findById(assessmentId)
115 1 1. lambda$setLocked$3 : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$setLocked$3 → KILLED
            .orElseThrow(() -> new EntityNotFoundException(PlAssessment.class, assessmentId));
116
117 1 1. setLocked : negated conditional → KILLED
    if (!Objects.equals(assessment.getPlRepoId(), course.getPlRepoId())
118 1 1. setLocked : negated conditional → KILLED
        || !Objects.equals(assessment.getPlInstanceId(), course.getPlInstanceId())) {
119
      throw new IllegalArgumentException(
120
          "assessmentId %d belongs to a different course".formatted(assessmentId));
121
    }
122
123 1 1. setLocked : removed call to edu/ucsb/cs/scaffold/entity/PlAssessment::setLocked → KILLED
    assessment.setLocked(locked);
124
    plAssessmentRepository.save(assessment);
125 1 1. setLocked : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::setLocked → KILLED
    return toAssessmentManagementDTO(assessment);
126
  }
127
128
  @Operation(
129
      summary = "List questions for an assessment, in the order they appear in the assessment")
130
  @GetMapping("/api/assessments/{assessmentId}/questions")
131
  public List<QuestionDTO> getQuestions(
132
      @Parameter(description = "Id of the PlAssessment") @PathVariable Long assessmentId) {
133
    List<PlAssessmentQuestion> joinRows =
134
        plAssessmentQuestionRepository.findByPlAssessmentIdOrderByOrdinalAsc(assessmentId);
135 1 1. getQuestions : negated conditional → KILLED
    if (joinRows.isEmpty()) {
136
      return List.of();
137
    }
138
139
    Map<Long, PlQuestion> questionsById =
140
        plQuestionRepository
141
            .findAllById(joinRows.stream().map(PlAssessmentQuestion::getPlQuestionId).toList())
142
            .stream()
143 1 1. lambda$getQuestions$4 : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getQuestions$4 → KILLED
            .collect(Collectors.toMap(PlQuestion::getId, q -> q));
144
145 1 1. getQuestions : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/AssessmentController::getQuestions → KILLED
    return joinRows.stream()
146 1 1. lambda$getQuestions$5 : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getQuestions$5 → KILLED
        .map(join -> questionsById.get(join.getPlQuestionId()))
147
        .filter(Objects::nonNull)
148 1 1. lambda$getQuestions$6 : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getQuestions$6 → KILLED
        .map(q -> toQuestionDTO(q, assessmentId))
149
        .toList();
150
  }
151
152
  private static AssessmentDTO toAssessmentDTO(PlAssessment a) {
153 1 1. toAssessmentDTO : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::toAssessmentDTO → KILLED
    return new AssessmentDTO(
154
        String.valueOf(a.getId()),
155 1 1. toAssessmentDTO : negated conditional → KILLED
        a.getPlAssessmentId() != null ? String.valueOf(a.getPlAssessmentId()) : null,
156
        assessmentLabel(a));
157
  }
158
159
  private static AssessmentManagementDTO toAssessmentManagementDTO(PlAssessment a) {
160 1 1. toAssessmentManagementDTO : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::toAssessmentManagementDTO → KILLED
    return new AssessmentManagementDTO(String.valueOf(a.getId()), assessmentLabel(a), a.isLocked());
161
  }
162
163
  private static String assessmentLabel(PlAssessment a) {
164 2 1. assessmentLabel : replaced return value with "" for edu/ucsb/cs/scaffold/controller/AssessmentController::assessmentLabel → KILLED
2. assessmentLabel : negated conditional → KILLED
    return a.getPlAssessmentTitle() != null ? a.getPlAssessmentTitle() : a.getName();
165
  }
166
167
  private static QuestionDTO toQuestionDTO(PlQuestion q, Long assessmentId) {
168 1 1. toQuestionDTO : replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::toQuestionDTO → KILLED
    return new QuestionDTO(
169
        String.valueOf(q.getId()),
170
        String.valueOf(assessmentId),
171
        q.getUuid().toString(),
172
        q.getTitle());
173
  }
174
175
  public record AssessmentDTO(
176
      String id, @JsonProperty("pl_assessment_id") String plAssessmentId, String name) {}
177
178
  public record AssessmentManagementDTO(String id, String name, boolean locked) {}
179
180
  public record QuestionDTO(
181
      String id,
182
      @JsonProperty("assessment_id") String assessmentId,
183
      @JsonProperty("pl_question_uuid") String plQuestionUuid,
184
      String title) {}
185
}

Mutations

52

1.1
Location : getAssessments
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_maps_and_orders_by_pl_assessment_order_then_name_and_excludes_locked()]
negated conditional → KILLED

2.2
Location : getAssessments
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_returns_empty_list_when_course_not_found()]
negated conditional → KILLED

3.3
Location : getAssessments
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_maps_and_orders_by_pl_assessment_order_then_name_and_excludes_locked()]
negated conditional → KILLED

58

1.1
Location : getAssessments
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_maps_and_orders_by_pl_assessment_order_then_name_and_excludes_locked()]
replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/AssessmentController::getAssessments → KILLED

61

1.1
Location : lambda$getAssessments$0
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_maps_and_orders_by_pl_assessment_order_then_name_and_excludes_locked()]
negated conditional → KILLED

2.2
Location : lambda$getAssessments$0
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_maps_and_orders_by_pl_assessment_order_then_name_and_excludes_locked()]
replaced boolean return with true for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getAssessments$0 → KILLED

81

1.1
Location : lambda$getAllAssessments$1
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAllAssessments_returns_404_when_course_not_found()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getAllAssessments$1 → KILLED

82

1.1
Location : getAllAssessments
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAllAssessments_returns_both_locked_and_unlocked_assessments()]
negated conditional → KILLED

2.2
Location : getAllAssessments
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAllAssessments_returns_both_locked_and_unlocked_assessments()]
negated conditional → KILLED

88

1.1
Location : getAllAssessments
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAllAssessments_returns_both_locked_and_unlocked_assessments()]
replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/AssessmentController::getAllAssessments → KILLED

111

1.1
Location : lambda$setLocked$2
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_returns_404_when_course_not_found()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$setLocked$2 → KILLED

115

1.1
Location : lambda$setLocked$3
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_returns_404_when_assessment_not_found()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$setLocked$3 → KILLED

117

1.1
Location : setLocked
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_can_lock_an_assessment()]
negated conditional → KILLED

118

1.1
Location : setLocked
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_returns_400_when_only_pl_instance_id_differs()]
negated conditional → KILLED

123

1.1
Location : setLocked
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_can_lock_an_assessment()]
removed call to edu/ucsb/cs/scaffold/entity/PlAssessment::setLocked → KILLED

125

1.1
Location : setLocked
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_can_lock_an_assessment()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::setLocked → KILLED

135

1.1
Location : getQuestions
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getQuestions_returns_questions_in_ordinal_order_and_skips_orphaned_join_rows()]
negated conditional → KILLED

143

1.1
Location : lambda$getQuestions$4
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getQuestions_returns_questions_in_ordinal_order_and_skips_orphaned_join_rows()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getQuestions$4 → KILLED

145

1.1
Location : getQuestions
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getQuestions_returns_questions_in_ordinal_order_and_skips_orphaned_join_rows()]
replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/AssessmentController::getQuestions → KILLED

146

1.1
Location : lambda$getQuestions$5
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getQuestions_returns_questions_in_ordinal_order_and_skips_orphaned_join_rows()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getQuestions$5 → KILLED

148

1.1
Location : lambda$getQuestions$6
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getQuestions_returns_questions_in_ordinal_order_and_skips_orphaned_join_rows()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::lambda$getQuestions$6 → KILLED

153

1.1
Location : toAssessmentDTO
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_maps_and_orders_by_pl_assessment_order_then_name_and_excludes_locked()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::toAssessmentDTO → KILLED

155

1.1
Location : toAssessmentDTO
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getAssessments_maps_and_orders_by_pl_assessment_order_then_name_and_excludes_locked()]
negated conditional → KILLED

160

1.1
Location : toAssessmentManagementDTO
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_can_lock_an_assessment()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::toAssessmentManagementDTO → KILLED

164

1.1
Location : assessmentLabel
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_can_lock_an_assessment()]
replaced return value with "" for edu/ucsb/cs/scaffold/controller/AssessmentController::assessmentLabel → KILLED

2.2
Location : assessmentLabel
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:setLocked_can_lock_an_assessment()]
negated conditional → KILLED

168

1.1
Location : toQuestionDTO
Killed by : edu.ucsb.cs.scaffold.controller.AssessmentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs.scaffold.controller.AssessmentControllerTests]/[method:getQuestions_returns_questions_in_ordinal_order_and_skips_orphaned_join_rows()]
replaced return value with null for edu/ucsb/cs/scaffold/controller/AssessmentController::toQuestionDTO → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0