StaffController.java
package edu.ucsb.cs156.happiercows.controllers;
import edu.ucsb.cs156.happiercows.entities.Staff;
import edu.ucsb.cs156.happiercows.errors.EntityNotFoundException;
import edu.ucsb.cs156.happiercows.models.CsvUploadResult;
import edu.ucsb.cs156.happiercows.models.StaffDTO;
import edu.ucsb.cs156.happiercows.repositories.StaffRepository;
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 = "Staff")
@RequestMapping("/api/staff")
@RestController
public class StaffController extends ApiController {
@Autowired
private StaffRepository staffRepository;
@Operation(summary = "List all course staff")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/all")
public Iterable<Staff> allStaff() {
return staffRepository.findAll();
}
@Operation(summary = "List the staff for a single course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/course/{courseId}")
public Iterable<Staff> staffForCourse(
@Parameter(name = "courseId") @PathVariable Long courseId) {
return staffRepository.findByCourseId(courseId);
}
@Operation(summary = "Get a staff member by id")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/{id}")
public Staff getStaffById(
@Parameter(name = "id") @PathVariable Long id) {
return staffRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(Staff.class, id));
}
@Operation(summary = "Add a staff member to a course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("")
public Staff postStaff(
@Parameter(name = "staff") @RequestBody StaffDTO staffDTO) {
return staffRepository.save(staffDTO.toStaff());
}
@Operation(summary = "Update a single staff member")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PutMapping("/{id}")
public Staff updateStaff(
@Parameter(name = "id") @PathVariable Long id, @RequestBody StaffDTO staffDTO) {
Staff staff = staffRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(Staff.class, id));
staff.setLastName(staffDTO.getLastName());
staff.setFirstMiddleName(staffDTO.getFirstMiddleName());
staff.setEmail(staffDTO.getEmail());
staff.setCourseId(staffDTO.getCourseId());
staffRepository.save(staff);
return staff;
}
@Operation(summary = "Remove a staff member from a course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping("/{id}")
public Object deleteStaff(
@Parameter(name = "id") @PathVariable Long id) {
Staff staff = staffRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(Staff.class, id));
staffRepository.delete(staff);
return genericMessage("Staff with id %s deleted".formatted(id));
}
private static final List<String> STAFF_CSV_HEADERS = List.of("lastName", "firstMiddleName", "email");
/**
* Bulk-adds staff to a course from an uploaded CSV file, with a single
* fixed header format ({@code lastName,firstMiddleName,email}). Rows
* whose email is already on the course's staff list are skipped rather
* than duplicated.
*
* @param courseId the course to add the staff to
* @param file the uploaded CSV file
* @return a summary of how many staff were created, and which emails
* were skipped as already-on-roster
*/
@Operation(summary = "Upload a CSV file of staff for a course")
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping(value = "/upload/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CsvUploadResult uploadStaffCsv(
@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.trim());
}
if (headerValues.size() != STAFF_CSV_HEADERS.size()) {
throw new IllegalArgumentException("Unrecognized CSV header format");
}
for (int i = 0; i < STAFF_CSV_HEADERS.size(); i++) {
if (!STAFF_CSV_HEADERS.get(i).equalsIgnoreCase(headerValues.get(i))) {
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() != STAFF_CSV_HEADERS.size()) {
throw new IllegalArgumentException(
"Row %d does not have the expected number of columns".formatted(row.getRecordNumber()));
}
Staff staff = Staff.builder()
.lastName(row.get(0).trim())
.firstMiddleName(row.get(1).trim())
.email(row.get(2).trim())
.courseId(courseId)
.build();
if (staffRepository.findByCourseIdAndEmail(courseId, staff.getEmail()).iterator().hasNext()) {
skippedEmails.add(staff.getEmail());
continue;
}
staffRepository.save(staff);
created++;
}
return CsvUploadResult.builder()
.created(created)
.skippedEmails(skippedEmails)
.build();
}
}