diff --git a/src/main/kotlin/org/dropProject/controllers/TeacherAPIController.kt b/src/main/kotlin/org/dropProject/controllers/TeacherAPIController.kt index 7869dd4f..1df3a0e4 100644 --- a/src/main/kotlin/org/dropProject/controllers/TeacherAPIController.kt +++ b/src/main/kotlin/org/dropProject/controllers/TeacherAPIController.kt @@ -39,6 +39,7 @@ import org.springframework.ui.ModelMap import org.springframework.web.bind.annotation.* import java.nio.file.Files import java.security.Principal +import java.util.* import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -65,7 +66,7 @@ class TeacherAPIController( val reportService: ReportService, val projectGroupRepository: ProjectGroupRepository, val assignmentACLRepository: AssignmentACLRepository, - val studentService: StudentService + val studentService: StudentService, ) { val LOG = LoggerFactory.getLogger(this.javaClass.name) @@ -88,11 +89,13 @@ class TeacherAPIController( value = "Get the latest submissions by each group for the assignment identified by the assignmentID variable", response = SubmissionInfo::class, responseContainer = "List", ignoreJsonView = false ) - fun getAssignmentLatestSubmissions(@PathVariable assignmentId: String, model: ModelMap, - principal: Principal, request: HttpServletRequest): ResponseEntity> { + fun getAssignmentLatestSubmissions( + @PathVariable assignmentId: String, model: ModelMap, + principal: Principal, request: HttpServletRequest + ): ResponseEntity> { assignmentService.getAllSubmissionsForAssignment(assignmentId, principal, model, request, mode = "summary") - val result = (model["submissions"] as List).map{ + val result = (model["submissions"] as List).map { AssignmentLatestSubmissionsResponse(it.projectGroup, it.lastSubmission, it.allSubmissions.size) } @@ -105,8 +108,10 @@ class TeacherAPIController( value = "", response = Submission::class, responseContainer = "List", ignoreJsonView = false ) - fun getGroupAssignmentSubmissions(@PathVariable assignmentId: String, @PathVariable groupId: Long, model: ModelMap, - principal: Principal, request: HttpServletRequest): ResponseEntity> { + fun getGroupAssignmentSubmissions( + @PathVariable assignmentId: String, @PathVariable groupId: Long, model: ModelMap, + principal: Principal, request: HttpServletRequest + ): ResponseEntity> { val submissions = submissionRepository .findByGroupAndAssignmentIdOrderBySubmissionDateDescStatusDateDesc(ProjectGroup(groupId), assignmentId) @@ -120,8 +125,10 @@ class TeacherAPIController( @GetMapping(value = ["/download/{submissionId}"], produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) @JsonView(JSONViews.TeacherAPI::class) @ApiOperation(value = "Download the mavenized zip file for this submission") - fun downloadProject(@PathVariable submissionId: Long, principal: Principal, - request: HttpServletRequest, response: HttpServletResponse): FileSystemResource { + fun downloadProject( + @PathVariable submissionId: Long, principal: Principal, + request: HttpServletRequest, response: HttpServletResponse + ): FileSystemResource { val submission = submissionRepository.findById(submissionId).orElse(null) if (submission != null) { @@ -131,8 +138,10 @@ class TeacherAPIController( throw AccessDeniedException("${principal.realName()} is not allowed to view this report") } - val projectFolder = assignmentTeacherFiles.getProjectFolderAsFile(submission, - wasRebuilt = submission.getStatus() == SubmissionStatus.VALIDATED_REBUILT) + val projectFolder = assignmentTeacherFiles.getProjectFolderAsFile( + submission, + wasRebuilt = submission.getStatus() == SubmissionStatus.VALIDATED_REBUILT + ) if (!Files.exists(projectFolder.toPath())) { throw ResourceNotFoundException() @@ -157,8 +166,10 @@ class TeacherAPIController( @GetMapping(value = ["/submissions/{submissionId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) @JsonView(JSONViews.TeacherAPI::class) @ApiOperation(value = "Get the build report associated with this submission") - fun getBuildReport(@PathVariable submissionId: Long, principal: Principal, - request: HttpServletRequest): ResponseEntity { + fun getBuildReport( + @PathVariable submissionId: Long, principal: Principal, + request: HttpServletRequest + ): ResponseEntity { val report = reportService.buildReport(submissionId, principal, request) @@ -172,11 +183,15 @@ class TeacherAPIController( @GetMapping(value = ["/studentHistory/{studentId}"], produces = [MediaType.APPLICATION_JSON_VALUE]) @JsonView(JSONViews.TeacherAPI::class) @ApiOperation(value = "Get the student's student history") - fun getStudentHistory(@PathVariable studentId: String, - principal: Principal, request: HttpServletRequest): ResponseEntity { - - return ResponseEntity.ok().body(studentService.getStudentHistory(studentId, principal) - ?: throw ResourceNotFoundException()) + fun getStudentHistory( + @PathVariable studentId: String, + principal: Principal, request: HttpServletRequest + ): ResponseEntity { + + return ResponseEntity.ok().body( + studentService.getStudentHistory(studentId, principal) + ?: throw ResourceNotFoundException() + ) } @GetMapping(value = ["/studentSearch/{query}"], produces = [MediaType.APPLICATION_JSON_VALUE]) @@ -205,10 +220,73 @@ class TeacherAPIController( return true } + @Suppress("UNCHECKED_CAST") + @GetMapping( + value = ["/assignments/{assignmentId}/previewMarkBestSubmissions"], + produces = [MediaType.APPLICATION_JSON_VALUE] + ) + @JsonView(JSONViews.TeacherAPI::class) + @ApiOperation(value = "Get a list of each group's best submissions for the assignment") + fun previewMarkBestSubmissions( + @PathVariable assignmentId: String, model: ModelMap, + @RequestParam ignoreOutsideOfDeadlineSubmissions: Boolean = false, + request: HttpServletRequest, principal: Principal + ): ResponseEntity> { + + assignmentService.getAllSubmissionsForAssignment(assignmentId, principal, model, request, mode = "summary") + + val groupSubmissions = (model["submissions"] as List).groupBy { it.projectGroup } + val bestSubmissions = mutableListOf() + + for (value in groupSubmissions.values) { + if (value.firstOrNull() == null + || value.first().allSubmissions.any + { it.submissionDate > ((model["assignment"] as Assignment).dueDate ?: Date()) } + ) { + + continue + } + + submissionService.fillIndicatorsFor(value.first().allSubmissions) + + fun getScore(submission: Submission) = + submission.teacherTests?.progress ?: 0 + + val sortedSubmissions = value.first().allSubmissions.sortedByDescending { getScore(it) } + val topScore = getScore(sortedSubmissions.first()) + + val bestSubmission = value.firstOrNull()?.allSubmissions + ?.filter { getScore(it) == topScore } + ?.maxByOrNull { it.submissionDate } + + if (bestSubmission != null) { + bestSubmissions.add(bestSubmission) + } + } + + return ResponseEntity.ok(bestSubmissions) + } + + @PostMapping(value = ["/markMultipleAsFinal"], + consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) + @JsonView(JSONViews.TeacherAPI::class) + @ApiOperation(value = "Mark multiple assignments as final") + fun markMultipleAsFinal(@RequestBody submissions: List, principal: Principal): Boolean { + + for (submission in submissions) { + markAsFinal(submission, principal) + } + + return true + } + @GetMapping(value = ["/assignmentSearch/{query}"], produces = [MediaType.APPLICATION_JSON_VALUE]) @JsonView(JSONViews.TeacherAPI::class) @ApiOperation(value = "Get all assignments that match the query value") - fun searchAssignments(@PathVariable("query") query: String, principal: Principal): ResponseEntity> { + fun searchAssignments( + @PathVariable("query") query: String, + principal: Principal + ): ResponseEntity> { val result = assignmentRepository.findAll() .filter { val acl = assignmentACLRepository.findByAssignmentId(it.id) @@ -225,4 +303,25 @@ class TeacherAPIController( return ResponseEntity(result, HttpStatus.OK) } + + @GetMapping(value = ["/assignments/{assignmentId}/toggleState"], produces = [MediaType.APPLICATION_JSON_VALUE]) + @JsonView(JSONViews.TeacherAPI::class) + @ApiOperation(value = "Deactivate the assignment") + fun toggleAssignmentState( + @PathVariable("assignmentId") assignmentId: String, + principal: Principal + ): Boolean { + + val assignment = assignmentRepository.findById(assignmentId).orElse(null) ?: return false + val acl = assignmentACLRepository.findByAssignmentId(assignmentId) + + if (principal.realName() != assignment.ownerUserId && acl.find { it.userId == principal.realName() } == null) { + return false + } + + assignment.active = !assignment.active + assignmentRepository.save(assignment) + + return true + } } diff --git a/src/test/kotlin/org/dropProject/controllers/TeacherAPIControllerTests.kt b/src/test/kotlin/org/dropProject/controllers/TeacherAPIControllerTests.kt index 028b86fd..55a5018e 100644 --- a/src/test/kotlin/org/dropProject/controllers/TeacherAPIControllerTests.kt +++ b/src/test/kotlin/org/dropProject/controllers/TeacherAPIControllerTests.kt @@ -40,11 +40,14 @@ import org.springframework.test.context.TestPropertySource import org.springframework.test.context.junit4.SpringRunner import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.transaction.annotation.Transactional import java.sql.Timestamp import java.time.LocalDateTime +import kotlin.test.assertFalse +import kotlin.test.assertTrue @RunWith(SpringRunner::class) @AutoConfigureMockMvc @@ -614,4 +617,72 @@ class TeacherAPIControllerTests: APIControllerTests { .header("authorization", testsHelper.header("teacher1", token))) .andExpect(status().isOk()).andExpect(content().string("false")) } + + @Test + @DirtiesContext + fun `try to mark the best submissions as final`() { + assigneeRepository.deleteAll() + assignmentRepository.deleteAll() + authorRepository.deleteAll() + + setup() + + val token = generateToken("teacher1", mutableListOf(SimpleGrantedAuthority("ROLE_TEACHER")), mvc) + + this.mvc.perform( + get("/api/teacher/assignments/testJavaProj/previewMarkBestSubmissions") + .contentType(MediaType.APPLICATION_JSON) + .header("authorization", testsHelper.header("teacher1", token))) + .andExpect(status().isOk()).andExpect(content().json(""" + [ + {"id":2, + "submissionDate":"2019-01-02T11:05:03.000+00:00", + "status":"VALIDATED", + "statusDate":"2019-01-02T11:05:03.000+00:00", + "markedAsFinal":false, + "teacherTests":{"numTests":4, + "numFailures":0, + "numErrors":0, + "numSkipped":0, + "ellapsed":0.007, + "numMandatoryOK":0, + "numMandatoryNOK":0 }, + "overdue":false, + "group":{"id":1, + "authors":[{"id":1, + "name":"Student 1"}]}}] + """.trimIndent())) + + this.mvc.perform( + post("/api/teacher/markMultipleAsFinal") + .contentType(MediaType.APPLICATION_JSON) + .content("[1, 2]") + .header("authorization", testsHelper.header("teacher1", token))) + .andExpect(status().isOk) + + val submissions = submissionRepository.findByStatusOrderByStatusDate(SubmissionStatus.VALIDATED.code) + .groupBy { it.id } + assertFalse { submissions[1]?.first()?.markedAsFinal == true } // first submission gets unmarked since 1 and 2 are made by the same student + assertTrue { submissions[2]?.first()?.markedAsFinal == true } + } + + @Test + @DirtiesContext + fun `try to deactivate an assignment`() { + assigneeRepository.deleteAll() + assignmentRepository.deleteAll() + authorRepository.deleteAll() + + setup() + + val token = generateToken("teacher1", mutableListOf(SimpleGrantedAuthority("ROLE_TEACHER")), mvc) + + this.mvc.perform( + get("/api/teacher/assignments/testJavaProj/toggleState") + .contentType(MediaType.APPLICATION_JSON) + .header("authorization", testsHelper.header("teacher1", token))) + .andExpect(status().isOk()).andExpect(content().string("true")) + + assertFalse { assignmentRepository.getReferenceById("testJavaProj").active } + } }