StudentController.java
package edu.ucsb.cs156.happiercows.controllers;
import edu.ucsb.cs156.happiercows.entities.Student;
import edu.ucsb.cs156.happiercows.errors.EntityNotFoundException;
import edu.ucsb.cs156.happiercows.helpers.StudentCsvFormat;
import edu.ucsb.cs156.happiercows.models.CsvUploadResult;
import edu.ucsb.cs156.happiercows.models.StudentDTO;
import edu.ucsb.cs156.happiercows.repositories.StudentRepository;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Slf4j
@Tag(name = "Student")
@RequestMapping("/api/student")
@RestController
public class StudentController extends ApiController {
@Autowired
private StudentRepository studentRepository;
@Operation(summary = "List all roster students")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/all")
public Iterable<Student> allStudents() {
return studentRepository.findAll();
}
@Operation(summary = "List the roster students for a single course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/course/{courseId}")
public Iterable<Student> studentsForCourse(
@Parameter(name = "courseId") @PathVariable Long courseId) {
return studentRepository.findByCourseId(courseId);
}
@Operation(summary = "Get a roster student by id")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/{id}")
public Student getStudentById(
@Parameter(name = "id") @PathVariable Long id) {
return studentRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(Student.class, id));
}
@Operation(summary = "Add a roster student to a course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("")
public Student postStudent(
@Parameter(name = "student") @RequestBody StudentDTO studentDTO) {
return studentRepository.save(studentDTO.toStudent());
}
@Operation(summary = "Update a single roster student")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PutMapping("/{id}")
public Student updateStudent(
@Parameter(name = "id") @PathVariable Long id, @RequestBody StudentDTO studentDTO) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(Student.class, id));
student.setLastName(studentDTO.getLastName());
student.setFirstMiddleName(studentDTO.getFirstMiddleName());
student.setEmail(studentDTO.getEmail());
student.setPerm(studentDTO.getPerm());
student.setCourseId(studentDTO.getCourseId());
studentRepository.save(student);
return student;
}
@Operation(summary = "Remove a roster student from a course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{id}")
public Object deleteStudent(
@Parameter(name = "id") @PathVariable Long id) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(Student.class, id));
studentRepository.delete(student);
return genericMessage("Student with id %s deleted".formatted(id));
}
/**
* Bulk-adds roster students to a course from an uploaded CSV file. The
* CSV's header row is used to auto-detect which school's roster export
* format it is (see {@link StudentCsvFormat}); rows whose email is
* already on the course roster are skipped rather than duplicated.
*
* @param courseId the course to add the students to
* @param file the uploaded CSV file
* @return a summary of how many students were created, and which emails
* were skipped as already-on-roster
*/
@Operation(summary = "Upload a CSV file of roster students for a course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping(value = "/upload/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CsvUploadResult uploadStudentsCsv(
@Parameter(name = "courseId") @RequestParam Long courseId,
@Parameter(name = "file") @RequestParam("file") MultipartFile file) throws IOException {
List<CSVRecord> records;
try (CSVParser parser = CSVFormat.DEFAULT.parse(
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
records = parser.getRecords();
}
if (records.isEmpty()) {
throw new IllegalArgumentException("CSV file is empty");
}
Iterator<CSVRecord> rows = records.iterator();
List<String> headerValues = new ArrayList<>();
for (String value : rows.next()) {
headerValues.add(value);
}
StudentCsvFormat format = StudentCsvFormat.detect(headerValues);
if (format == null) {
throw new IllegalArgumentException("Unrecognized CSV header format");
}
int created = 0;
List<String> skippedEmails = new ArrayList<>();
while (rows.hasNext()) {
CSVRecord row = rows.next();
if (row.size() != format.getHeaders().size()) {
throw new IllegalArgumentException(
"Row %d does not have the expected number of columns".formatted(row.getRecordNumber()));
}
Student student = format.toStudent(row, courseId);
if (studentRepository.findByCourseIdAndEmail(courseId, student.getEmail()).iterator().hasNext()) {
skippedEmails.add(student.getEmail());
continue;
}
studentRepository.save(student);
created++;
}
return CsvUploadResult.builder()
.created(created)
.skippedEmails(skippedEmails)
.build();
}
}