ConceptsYamlController.java
package edu.ucsb.cs.scaffold.controller;
import edu.ucsb.cs.scaffold.errors.EntityNotFoundException;
import edu.ucsb.cs.scaffold.services.ConceptYamlService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* Download and upload a course's entire concept-graph content (concepts, subconcepts, prerequisite
* edges, and practice problems) as a YAML file. See docs/yaml-format.md for the format.
*/
@Tag(name = "Concepts YAML")
@RestController
@RequiredArgsConstructor
public class ConceptsYamlController extends ApiController {
private final ConceptYamlService conceptYamlService;
@Operation(
summary = "Download the course's concept-graph content as a YAML file",
description =
"""
Produces the course's concepts, subconcepts, prerequisite edges, and practice
problems as a human-editable YAML document (see docs/yaml-format.md). The file
can be edited and uploaded to this or another course.
""")
@PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
@GetMapping("/api/concepts/yaml/download")
public ResponseEntity<String> downloadConceptsYaml(
@Parameter(name = "courseId") @RequestParam Long courseId)
throws EntityNotFoundException, IOException {
String yaml = conceptYamlService.createYAML(courseId);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/x-yaml"))
.header(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"concepts-course-%d.yaml\"".formatted(courseId))
.body(yaml);
}
@Operation(
summary = "Replace the course's concept-graph content from an uploaded YAML file",
description =
"""
Replaces ALL of the course's concepts, subconcepts, prerequisite edges, and
practice problems with the uploaded document (see docs/yaml-format.md), and
deletes every user's saved per-course scaffold state, which would be stale.
All-or-nothing: an invalid file changes nothing and responds 400 with every
problem found listed under errors.
""")
@PreAuthorize("@CourseSecurity.hasManagePermissions(#root, #courseId)")
@PostMapping(
value = "/api/concepts/yaml/upload",
consumes = {"multipart/form-data"})
public ResponseEntity<Map<String, Object>> uploadConceptsYaml(
@Parameter(name = "courseId") @RequestParam Long courseId,
@Parameter(name = "file") @RequestParam("file") MultipartFile file)
throws EntityNotFoundException, IOException {
Map<String, Object> report =
conceptYamlService.replaceFromYAML(courseId, file.getInputStream());
HttpStatus status =
Boolean.TRUE.equals(report.get("success")) ? HttpStatus.OK : HttpStatus.BAD_REQUEST;
return ResponseEntity.status(status).body(report);
}
}