StudentController.java

1
package edu.ucsb.cs156.happiercows.controllers;
2
3
import edu.ucsb.cs156.happiercows.entities.Student;
4
import edu.ucsb.cs156.happiercows.errors.EntityNotFoundException;
5
import edu.ucsb.cs156.happiercows.helpers.StudentCsvFormat;
6
import edu.ucsb.cs156.happiercows.models.CsvUploadResult;
7
import edu.ucsb.cs156.happiercows.models.StudentDTO;
8
import edu.ucsb.cs156.happiercows.repositories.StudentRepository;
9
import io.swagger.v3.oas.annotations.tags.Tag;
10
import io.swagger.v3.oas.annotations.Operation;
11
import io.swagger.v3.oas.annotations.Parameter;
12
import lombok.extern.slf4j.Slf4j;
13
import org.apache.commons.csv.CSVFormat;
14
import org.apache.commons.csv.CSVParser;
15
import org.apache.commons.csv.CSVRecord;
16
import org.springframework.beans.factory.annotation.Autowired;
17
import org.springframework.http.MediaType;
18
import org.springframework.security.access.prepost.PreAuthorize;
19
import org.springframework.web.bind.annotation.*;
20
import org.springframework.web.multipart.MultipartFile;
21
22
import java.io.IOException;
23
import java.io.InputStreamReader;
24
import java.nio.charset.StandardCharsets;
25
import java.util.ArrayList;
26
import java.util.Iterator;
27
import java.util.List;
28
29
@Slf4j
30
@Tag(name = "Student")
31
@RequestMapping("/api/student")
32
@RestController
33
public class StudentController extends ApiController {
34
    @Autowired
35
    private StudentRepository studentRepository;
36
37
    @Operation(summary = "List all roster students")
38
    @PreAuthorize("hasRole('ROLE_ADMIN')")
39
    @GetMapping("/all")
40
    public Iterable<Student> allStudents() {
41 1 1. allStudents : replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StudentController::allStudents → KILLED
        return studentRepository.findAll();
42
    }
43
44
    @Operation(summary = "List the roster students for a single course")
45
    @PreAuthorize("hasRole('ROLE_ADMIN')")
46
    @GetMapping("/course/{courseId}")
47
    public Iterable<Student> studentsForCourse(
48
            @Parameter(name = "courseId") @PathVariable Long courseId) {
49 1 1. studentsForCourse : replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StudentController::studentsForCourse → KILLED
        return studentRepository.findByCourseId(courseId);
50
    }
51
52
    @Operation(summary = "Get a roster student by id")
53
    @PreAuthorize("hasRole('ROLE_ADMIN')")
54
    @GetMapping("/{id}")
55
    public Student getStudentById(
56
            @Parameter(name = "id") @PathVariable Long id) {
57 1 1. getStudentById : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::getStudentById → KILLED
        return studentRepository.findById(id)
58 1 1. lambda$getStudentById$0 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::lambda$getStudentById$0 → KILLED
                .orElseThrow(() -> new EntityNotFoundException(Student.class, id));
59
    }
60
61
    @Operation(summary = "Add a roster student to a course")
62
    @PreAuthorize("hasRole('ROLE_ADMIN')")
63
    @PostMapping("")
64
    public Student postStudent(
65
            @Parameter(name = "student") @RequestBody StudentDTO studentDTO) {
66 1 1. postStudent : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::postStudent → KILLED
        return studentRepository.save(studentDTO.toStudent());
67
    }
68
69
    @Operation(summary = "Update a single roster student")
70
    @PreAuthorize("hasRole('ROLE_ADMIN')")
71
    @PutMapping("/{id}")
72
    public Student updateStudent(
73
            @Parameter(name = "id") @PathVariable Long id, @RequestBody StudentDTO studentDTO) {
74
        Student student = studentRepository.findById(id)
75 1 1. lambda$updateStudent$1 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::lambda$updateStudent$1 → KILLED
                .orElseThrow(() -> new EntityNotFoundException(Student.class, id));
76
77 1 1. updateStudent : removed call to edu/ucsb/cs156/happiercows/entities/Student::setLastName → KILLED
        student.setLastName(studentDTO.getLastName());
78 1 1. updateStudent : removed call to edu/ucsb/cs156/happiercows/entities/Student::setFirstMiddleName → KILLED
        student.setFirstMiddleName(studentDTO.getFirstMiddleName());
79 1 1. updateStudent : removed call to edu/ucsb/cs156/happiercows/entities/Student::setEmail → KILLED
        student.setEmail(studentDTO.getEmail());
80 1 1. updateStudent : removed call to edu/ucsb/cs156/happiercows/entities/Student::setPerm → KILLED
        student.setPerm(studentDTO.getPerm());
81 1 1. updateStudent : removed call to edu/ucsb/cs156/happiercows/entities/Student::setCourseId → KILLED
        student.setCourseId(studentDTO.getCourseId());
82
83
        studentRepository.save(student);
84
85 1 1. updateStudent : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::updateStudent → KILLED
        return student;
86
    }
87
88
    @Operation(summary = "Remove a roster student from a course")
89
    @PreAuthorize("hasRole('ROLE_ADMIN')")
90
    @DeleteMapping("/{id}")
91
    public Object deleteStudent(
92
            @Parameter(name = "id") @PathVariable Long id) {
93
        Student student = studentRepository.findById(id)
94 1 1. lambda$deleteStudent$2 : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::lambda$deleteStudent$2 → KILLED
                .orElseThrow(() -> new EntityNotFoundException(Student.class, id));
95
96 1 1. deleteStudent : removed call to edu/ucsb/cs156/happiercows/repositories/StudentRepository::delete → KILLED
        studentRepository.delete(student);
97 1 1. deleteStudent : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::deleteStudent → KILLED
        return genericMessage("Student with id %s deleted".formatted(id));
98
    }
99
100
    /**
101
     * Bulk-adds roster students to a course from an uploaded CSV file. The
102
     * CSV's header row is used to auto-detect which school's roster export
103
     * format it is (see {@link StudentCsvFormat}); rows whose email is
104
     * already on the course roster are skipped rather than duplicated.
105
     *
106
     * @param courseId the course to add the students to
107
     * @param file the uploaded CSV file
108
     * @return a summary of how many students were created, and which emails
109
     *         were skipped as already-on-roster
110
     */
111
    @Operation(summary = "Upload a CSV file of roster students for a course")
112
    @PreAuthorize("hasRole('ROLE_ADMIN')")
113
    @PostMapping(value = "/upload/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
114
    public CsvUploadResult uploadStudentsCsv(
115
            @Parameter(name = "courseId") @RequestParam Long courseId,
116
            @Parameter(name = "file") @RequestParam("file") MultipartFile file) throws IOException {
117
118
        List<CSVRecord> records;
119
        try (CSVParser parser = CSVFormat.DEFAULT.parse(
120
                new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
121
            records = parser.getRecords();
122
        }
123
124 1 1. uploadStudentsCsv : negated conditional → KILLED
        if (records.isEmpty()) {
125
            throw new IllegalArgumentException("CSV file is empty");
126
        }
127
128
        Iterator<CSVRecord> rows = records.iterator();
129
130
        List<String> headerValues = new ArrayList<>();
131
        for (String value : rows.next()) {
132
            headerValues.add(value);
133
        }
134
135
        StudentCsvFormat format = StudentCsvFormat.detect(headerValues);
136 1 1. uploadStudentsCsv : negated conditional → KILLED
        if (format == null) {
137
            throw new IllegalArgumentException("Unrecognized CSV header format");
138
        }
139
140
        int created = 0;
141
        List<String> skippedEmails = new ArrayList<>();
142
143 1 1. uploadStudentsCsv : negated conditional → KILLED
        while (rows.hasNext()) {
144
            CSVRecord row = rows.next();
145 1 1. uploadStudentsCsv : negated conditional → KILLED
            if (row.size() != format.getHeaders().size()) {
146
                throw new IllegalArgumentException(
147
                        "Row %d does not have the expected number of columns".formatted(row.getRecordNumber()));
148
            }
149
150
            Student student = format.toStudent(row, courseId);
151
152 1 1. uploadStudentsCsv : negated conditional → KILLED
            if (studentRepository.findByCourseIdAndEmail(courseId, student.getEmail()).iterator().hasNext()) {
153
                skippedEmails.add(student.getEmail());
154
                continue;
155
            }
156
157
            studentRepository.save(student);
158 1 1. uploadStudentsCsv : Changed increment from 1 to -1 → KILLED
            created++;
159
        }
160
161 1 1. uploadStudentsCsv : replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::uploadStudentsCsv → KILLED
        return CsvUploadResult.builder()
162
                .created(created)
163
                .skippedEmails(skippedEmails)
164
                .build();
165
    }
166
}

Mutations

41

1.1
Location : allStudents
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_get_all_students()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StudentController::allStudents → KILLED

49

1.1
Location : studentsForCourse
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_get_students_for_a_course()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/happiercows/controllers/StudentController::studentsForCourse → KILLED

57

1.1
Location : getStudentById
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_get_student_by_id()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::getStudentById → KILLED

58

1.1
Location : lambda$getStudentById$0
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_cannot_get_student_when_it_does_not_exist()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::lambda$getStudentById$0 → KILLED

66

1.1
Location : postStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_post_new_student()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::postStudent → KILLED

75

1.1
Location : lambda$updateStudent$1
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_cannot_edit_student_that_does_not_exist()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::lambda$updateStudent$1 → KILLED

77

1.1
Location : updateStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_edit_an_existing_student()]
removed call to edu/ucsb/cs156/happiercows/entities/Student::setLastName → KILLED

78

1.1
Location : updateStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_edit_an_existing_student()]
removed call to edu/ucsb/cs156/happiercows/entities/Student::setFirstMiddleName → KILLED

79

1.1
Location : updateStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_edit_an_existing_student()]
removed call to edu/ucsb/cs156/happiercows/entities/Student::setEmail → KILLED

80

1.1
Location : updateStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_edit_an_existing_student()]
removed call to edu/ucsb/cs156/happiercows/entities/Student::setPerm → KILLED

81

1.1
Location : updateStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_edit_an_existing_student()]
removed call to edu/ucsb/cs156/happiercows/entities/Student::setCourseId → KILLED

85

1.1
Location : updateStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_edit_an_existing_student()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::updateStudent → KILLED

94

1.1
Location : lambda$deleteStudent$2
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_tries_to_delete_non_existant_student_and_gets_right_error_message()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::lambda$deleteStudent$2 → KILLED

96

1.1
Location : deleteStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_delete_a_student()]
removed call to edu/ucsb/cs156/happiercows/repositories/StudentRepository::delete → KILLED

97

1.1
Location : deleteStudent
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_delete_a_student()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::deleteStudent → KILLED

124

1.1
Location : uploadStudentsCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:empty_csv_file_is_rejected()]
negated conditional → KILLED

136

1.1
Location : uploadStudentsCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:unrecognized_header_format_is_rejected()]
negated conditional → KILLED

143

1.1
Location : uploadStudentsCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:malformed_row_with_too_many_columns_is_rejected()]
negated conditional → KILLED

145

1.1
Location : uploadStudentsCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:malformed_row_with_too_many_columns_is_rejected()]
negated conditional → KILLED

152

1.1
Location : uploadStudentsCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_upload_the_real_egrades_csv_example_file()]
negated conditional → KILLED

158

1.1
Location : uploadStudentsCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_upload_the_real_egrades_csv_example_file()]
Changed increment from 1 to -1 → KILLED

161

1.1
Location : uploadStudentsCsv
Killed by : edu.ucsb.cs156.happiercows.controllers.StudentControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.happiercows.controllers.StudentControllerTests]/[method:admin_can_upload_the_real_egrades_csv_example_file()]
replaced return value with null for edu/ucsb/cs156/happiercows/controllers/StudentController::uploadStudentsCsv → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0