diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5476f1d9e02fe2ab51d972a6d9f0c1385276e50b..f8f5b80ad912300f4bd05861a54f8f1cd483b8c1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,7 +63,7 @@ gradle_build: stage: build rules: - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID || $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "trigger" @@ -91,7 +91,7 @@ gradle_test: rules: - if: $CI_PIPELINE_SOURCE == "trigger" when: never - - if: $CI_COMMIT_BRANCH == "dev" || + - if: $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID || $CI_PIPELINE_SOURCE == "push" coverage: '/Code coverage: \d+\.\d+/' @@ -128,7 +128,7 @@ mysql_migration: - if: $CI_PIPELINE_SOURCE == "trigger" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID || $CI_PIPELINE_SOURCE == "push" stage: prepare @@ -170,7 +170,7 @@ postgreSQL_migration: - if: $CI_PIPELINE_SOURCE == "trigger" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID || $CI_PIPELINE_SOURCE == "push" stage: prepare @@ -215,7 +215,7 @@ mysql_test: # Run on development and master branches. - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" + $CI_COMMIT_BRANCH == "development" # Only run on merge requests that change one of the model/migration files otherwise. - if: $CI_PIPELINE_SOURCE != "merge_request_event" @@ -256,7 +256,7 @@ mysql_test: # # # Always run on development and master branches. # - if: $CI_COMMIT_BRANCH == "master" || -# $CI_COMMIT_BRANCH == "dev" +# $CI_COMMIT_BRANCH == "development" # # # Only run on merge requests that change one of the model/migration files otherwise. # - if: $CI_PIPELINE_SOURCE != "merge_request_event" @@ -301,7 +301,7 @@ gradle_spotless: - if: $CI_PIPELINE_SOURCE == "trigger" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID || $CI_PIPELINE_SOURCE == "push" artifacts: @@ -327,7 +327,7 @@ gradle_licenses: - if: $CI_PIPELINE_SOURCE == "trigger" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID || $CI_PIPELINE_SOURCE == "push" stage: review @@ -346,7 +346,7 @@ publish_jar: stage: publish rules: - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID || $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "trigger" @@ -448,7 +448,7 @@ code_quality: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID stage: report @@ -462,7 +462,7 @@ spotbugs-sast: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID stage: report needs: @@ -480,7 +480,7 @@ dast: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_COMMIT_BRANCH == "development" when: manual stage: live report variables: @@ -499,7 +499,7 @@ secret_detection: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID stage: report @@ -513,7 +513,7 @@ gemnasium-maven-dependency_scanning: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - if: $CI_COMMIT_BRANCH == "master" || - $CI_COMMIT_BRANCH == "dev" || + $CI_COMMIT_BRANCH == "development" || $CI_MERGE_REQUEST_ID needs: - gradle_build @@ -526,7 +526,7 @@ a11y: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_COMMIT_BRANCH == "development" stage: live report variables: ally_urls: "https://submit.eiptest.ewi.tudelft.nl" @@ -542,7 +542,7 @@ browser_performance: - if: $CI_PIPELINE_SOURCE == "trigger" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" when: never - - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_COMMIT_BRANCH == "development" stage: live report variables: URL: "https://submit.eiptest.ewi.tudelft.nl" diff --git a/CHANGELOG.md b/CHANGELOG.md index fe87914e690eb086eca1b13299ae68b3ea10ce1b..67103445f6d0c7ef294c4316d19022bc09e661cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [2.2.1] + +### Added +- Ability to import groups from csv file (@dsavvidi) +- Markdown support for script feedback (@rwbackx) + +### Changed + +### Fixed +- Fixed bug where assistant view resets selected module upon search (@dsavvidi) +- Fixed bug where module grades didn't work when name started with a number (@dsavvidi) + +## [2.2.0] ### Added - Added delete grade button in group page submissions (@dsavvidi) - Ability to change group size for all groups (@dsavvidi) @@ -20,7 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added markdown support when giving feedback (@dsavvidi) - Module level grade export features (@dsavvidi) - Can now view the submission content in browser (@mmadara) +- Ability to show/hide assignments (@dsavvidi) - New statistic: Average number of submissions before passing per assignment (@dsavvidi) +- Ability to make test submissions for teachers and head TAs (@dsavvidi) ### Changed - Group joining is now restricted to groups with no submissions (@dsavvidi) @@ -30,7 +46,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changed the 'download selected submissions' button into 'download all' and 'download latest' (@mmadara) ### Fixed +- The dropdown for groups in the Assistant view doesn't reload the page anymore(@mmadara) - Modules and assignments were not sorted (@dsavvidi) +- Students could be a part of multiple groups in a module (@aturgut) - Fixed bug where changing the staff role did not work (@dsavvidi) - Email link to settings was hardcoded (@dsavvidi) diff --git a/build.gradle.kts b/build.gradle.kts index edf0679cf8cd85731d4fec58a1795ee613610850..262a0c9141208940cc9b73986c831b41320a0bcc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,9 +2,10 @@ import com.diffplug.gradle.spotless.SpotlessExtension import org.springframework.boot.gradle.tasks.bundling.BootJar import nl.javadude.gradle.plugins.license.DownloadLicensesExtension import nl.javadude.gradle.plugins.license.LicenseExtension +import java.nio.file.Files group = "nl.tudelft.submit" -version = "2.2.0" +version = "2.2.1" val javaVersion = JavaVersion.VERSION_17 @@ -71,8 +72,6 @@ plugins { // Open API generator for generating the Client code for Labracore. // is this needed? // id("org.openapi.generator").version("4.2.3") - - id("com.unclezs.gradle.sass").version("1.0.10") } sourceSets { @@ -173,8 +172,6 @@ val jacocoTestReport by tasks.getting(JacocoReport::class) { // Task for generating the client code for connecting with the Labracore API // val generateSubmitClient? -val processResources by tasks.getting(ProcessResources::class) - // Configure Spring Boot plugin task for running the application. val bootJar by tasks.getting(BootJar::class) { enabled = true @@ -218,9 +215,40 @@ tasks.withType<Test>().configureEach { } // Configure the sass compiling task to use the sass and css directories. -sass { - cssPath = "static/css" - sassPath = "scss" +tasks.register("ensureDirectory") { + // Store target directory into a variable to avoid project reference in the configuration cache + val directories = listOf(file("src/main/resources/static/css"), file("src/main/resources/scss")) + + doLast { + directories.forEach { directory -> + Files.createDirectories(directory.toPath()) + } + } +} +task<Exec>("sassCompile") { + dependsOn.add(tasks.getByName("ensureDirectory")) + if (System.getProperty("os.name").contains("windows",true)) { + commandLine("cmd", "/c", "sass", "src/main/resources/scss:src/main/resources/static/css") + } else { + commandLine("echo", "Checking for sass or sassc...") + doLast { + val res = exec { + isIgnoreExitValue = true + executable = "bash" + args = listOf("-l", "-c", "sass --version") + } + if (res.exitValue == 0) { + exec { commandLine("sass", "src/main/resources/scss:src/main/resources/static/css") } + } else { + File("src/main/resources/scss").listFiles()!!.filter { it.extension == "scss" && !it.name.startsWith("_") }.forEach { + exec { commandLine("sassc", "src/main/resources/scss/${it.name}", "src/main/resources/static/css/${it.nameWithoutExtension}.css") } + } + } + } + } +} +val processResources by tasks.getting(ProcessResources::class) { + dependsOn.add(tasks.getByName("sassCompile")) } dependencies { @@ -236,6 +264,9 @@ dependencies { // Guava implementation("com.google.guava:guava:11.0.2") + // Apache Commons + implementation("org.apache.commons:commons-csv:1.10.0") + implementation("com.github.ulisesbocchio:spring-boot-security-saml:$samlVersion") // Hibernate for database-entity mapping and EntityManagers diff --git a/src/main/java/nl/tudelft/submit/controller/AssignmentController.java b/src/main/java/nl/tudelft/submit/controller/AssignmentController.java index efd210c21be6bf2e56e82ab158e44fbbd52a5389..9e8d86c4219624619b395588fe98ed7c2d60e36e 100644 --- a/src/main/java/nl/tudelft/submit/controller/AssignmentController.java +++ b/src/main/java/nl/tudelft/submit/controller/AssignmentController.java @@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -51,6 +52,7 @@ import com.opencsv.exceptions.CsvValidationException; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.librador.dto.view.View; import nl.tudelft.submit.cache.PersonCacheManager; import nl.tudelft.submit.cache.StudentGroupCacheManager; import nl.tudelft.submit.dto.create.SubmitAssignmentCreateDTO; @@ -70,8 +72,10 @@ import nl.tudelft.submit.dto.view.FeedbackViewDTO; import nl.tudelft.submit.dto.view.VersionViewDTO; import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO; import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO; +import nl.tudelft.submit.dto.view.script.ScriptTrainResultViewDTO; import nl.tudelft.submit.external.PageUtil; import nl.tudelft.submit.model.Signature; +import nl.tudelft.submit.model.TestSubmission; import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.*; @@ -166,6 +170,15 @@ public class AssignmentController { model.addAttribute("version", version); model.addAttribute("availableGroups", availableGroups); + Optional<TestSubmission> sub = submissionService + .getLatestTestSubmissionByAssignment(assignment.getId()); + if (sub.isPresent()) { + List<ScriptTrainResultViewDTO> scriptResults = View.convert(sub.get().getScriptResults(), + ScriptTrainResultViewDTO.class); + model.addAttribute("testSubmission", sub.get()); + model.addAttribute("testScriptResults", scriptResults); + } + if (isTransparent) { model.addAttribute("train", scriptService.getScriptTrainById(versions.get(0).getScriptTrain().getId())); @@ -360,4 +373,17 @@ public class AssignmentController { return "redirect:/assignment/{id}"; } + /** + * Toggles the visibility of an assignment. + * + * @param id The id of the assignment. + * @return The page to load + */ + @PostMapping("{id}/change-visibility") + @PreAuthorize("@authorizationService.canEditAssignment(#id)") + public String toggleHide(@PathVariable Long id) { + assignmentService.toggleHide(id); + return "redirect:/assignment/" + id; + } + } diff --git a/src/main/java/nl/tudelft/submit/controller/GradeController.java b/src/main/java/nl/tudelft/submit/controller/GradeController.java index d1da7681e4c7890d13796b28f23a4e90e189d17a..dfcfbcd3fb19961a446e367541fbd782611e1054 100644 --- a/src/main/java/nl/tudelft/submit/controller/GradeController.java +++ b/src/main/java/nl/tudelft/submit/controller/GradeController.java @@ -42,6 +42,7 @@ import nl.tudelft.submit.model.grading.CalculatedScore; import nl.tudelft.submit.model.grading.GradingFormula; import nl.tudelft.submit.service.AssignmentService; import nl.tudelft.submit.service.GradeService; +import nl.tudelft.submit.service.SubmissionService; @Transactional @RequestMapping("/grade") @@ -52,6 +53,8 @@ public class GradeController { private GradeService gradeService; @Autowired private AssignmentService assignmentService; + @Autowired + private SubmissionService submissionService; @PostMapping("/group/{groupId}/{assignmentId}") @PreAuthorize("@authorizationService.canGradeSubmission(#create.submissionId)") @@ -61,7 +64,7 @@ public class GradeController { gradeService.addGradedFeedback(person.getId(), assignmentId, create); return "redirect:/group/{groupId}?assignment={assignmentId}&submission=" - + create.getSubmissionId(); + + submissionService.getSubmissionIdByCoreId(create.getSubmissionId()); } @PostMapping("/edition/{editionId}/{assignmentId}") diff --git a/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java b/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java index 70f41e031782b41e0fa92af94578874e553d6663..5a9b94344a89b557b4ac7d0ddad516198cffde56 100644 --- a/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java +++ b/src/main/java/nl/tudelft/submit/controller/StudentGroupController.java @@ -17,6 +17,7 @@ */ package nl.tudelft.submit.controller; +import java.io.IOException; import java.time.LocalDateTime; import java.util.*; import java.util.function.Function; @@ -30,6 +31,10 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import com.opencsv.exceptions.CsvValidationException; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; @@ -41,6 +46,7 @@ import nl.tudelft.submit.dto.create.note.GroupNoteCreateDTO; import nl.tudelft.submit.dto.create.note.SubmissionNoteCreateDTO; import nl.tudelft.submit.dto.id.SubmissionId; import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO; +import nl.tudelft.submit.exception.GroupSwitchingException; import nl.tudelft.submit.model.Signature; import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.*; @@ -182,10 +188,16 @@ public class StudentGroupController { @PostMapping @PreAuthorize("@authorizationService.canCreateGroup(#create.getModule().getId())") public String createGroup(@AuthenticatedPerson Person person, - @ModelAttribute("groupCreate") StudentGroupCreateDTO create) { - Long id = groupService.createGroup(create); - - return "redirect:/group/" + id; + @ModelAttribute("groupCreate") StudentGroupCreateDTO create, + RedirectAttributes redirectAttributes) { + try { + Long id = groupService.createGroup(create); + return "redirect:/group/" + id; + } catch (GroupSwitchingException e) { + redirectAttributes.addFlashAttribute("error", e.getMessage()); + + return "redirect:/module/" + create.getModule().getId() + "/groups"; + } } /** @@ -221,6 +233,21 @@ public class StudentGroupController { return "redirect:/module/" + moduleId + "/groups"; } + /** + * Imports student groups from a csv file. + * + * @param moduleId the id of the module to which these groups will belong + * @param file the csv file containing the groups + * @return a redirect to getGroupsPerModule() + */ + @PostMapping("{moduleId}/import") + @PreAuthorize("@authorizationService.canImportStudentGroups(#moduleId)") + public String importStudentGroups(@PathVariable Long moduleId, + @RequestParam("file") MultipartFile file) throws IOException, CsvValidationException { + groupService.importStudentGroups(moduleId, file); + return "redirect:/module/" + moduleId + "/groups"; + } + /** * Edits group capacity for a module. * @@ -246,10 +273,18 @@ public class StudentGroupController { */ @PostMapping("/{id}/join") @PreAuthorize("@authorizationService.canJoinGroup(#id)") - public String joinGroup(@AuthenticatedPerson Person person, @PathVariable Long id) { - groupService.addPeopleToGroup(id, List.of(person.getUsername())); - + public String joinGroup(@AuthenticatedPerson Person person, @PathVariable Long id, + RedirectAttributes redirectAttributes) { ModuleDetailsDTO module = moduleService.getModuleById(groupService.getModuleIdFromGroup(id)); + try { + groupService.addPeopleToGroup(id, List.of(person.getUsername())); + + } catch (GroupSwitchingException e) { + redirectAttributes.addFlashAttribute("error", e.getMessage()); + + return "redirect:/module/" + module.getId() + "/groups"; + } + return "redirect:/edition/" + module.getEdition().getId() + "/student?module=" + module.getId(); } @@ -262,8 +297,13 @@ public class StudentGroupController { */ @PostMapping("/{id}/members") @PreAuthorize("@authorizationService.canAddMembersToGroup(#id)") - public String addMembersToGroup(@PathVariable Long id, @RequestParam List<String> usernames) { - groupService.addPeopleToGroup(id, usernames); + public String addMembersToGroup(@PathVariable Long id, @RequestParam List<String> usernames, + RedirectAttributes redirectAttributes) { + try { + groupService.addPeopleToGroup(id, usernames); + } catch (GroupSwitchingException e) { + redirectAttributes.addFlashAttribute("error", e.getMessage()); + } return "redirect:/group/" + id; } diff --git a/src/main/java/nl/tudelft/submit/controller/SubmissionController.java b/src/main/java/nl/tudelft/submit/controller/SubmissionController.java index d106fb41b6a43745d8a28de5230c61f8e08620da..465558d5c837aa753fc43f22a4227c5c62770289 100644 --- a/src/main/java/nl/tudelft/submit/controller/SubmissionController.java +++ b/src/main/java/nl/tudelft/submit/controller/SubmissionController.java @@ -41,6 +41,7 @@ import org.springframework.web.multipart.MultipartHttpServletRequest; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson; import nl.tudelft.labracore.lib.security.user.Person; +import nl.tudelft.librador.dto.view.View; import nl.tudelft.submit.dto.create.MailMessageCreateDTO; import nl.tudelft.submit.dto.create.SubmissionDownloadConfigCreateDTO; import nl.tudelft.submit.dto.create.note.SubmissionNoteCreateDTO; @@ -48,7 +49,9 @@ import nl.tudelft.submit.dto.view.FileViewDTO; import nl.tudelft.submit.dto.view.VersionViewDTO; import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO; import nl.tudelft.submit.exception.DisallowedUploadException; +import nl.tudelft.submit.model.CoreSubmission; import nl.tudelft.submit.model.Signature; +import nl.tudelft.submit.model.TestSubmission; import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.*; @@ -98,6 +101,48 @@ public class SubmissionController { @Autowired private AuthorizationService authorizationService; + /** + * Adds and uploads a newly created test submission. + * + * @param assignmentId the assignment id + * @param versionId the id of the version to run the test submission on + * @param request the request + * @return redirection to the assignment page + */ + @Validated + @PostMapping(value = "/test-submission/{assignmentId}", consumes = { "multipart/form-data" }) + @PreAuthorize("@authorizationService.canSubmitTestSubmission(#assignmentId)") + public String addTestSubmission(@PathVariable Long assignmentId, + @RequestParam(name = "versionId", required = false) Long versionId, + MultipartHttpServletRequest request) { + AssignmentDetailsDTO assignment = assignmentService.getAssignmentDetails(assignmentId); + String submissionsDirectory = storageDir + "/submissions"; + + try { + if (!submissionService.checkSubmission(assignment.getAllowedFileTypes(), request.getFileMap())) + throw new DisallowedUploadException("One or more of the submitted files are not allowed"); + + TestSubmission submission = submissionService + .createTestSubmissionForAssignment(assignment.getId()); + + fileService.uploadFiles(request.getFileMap(), submission.getId(), submissionsDirectory); + + VersionViewDTO version; + if (versionId == null) { + version = versionService.getVersionsPerAssignment(assignmentId).get(0); + } else { + version = View.convert(versionService.getVersion(versionId), VersionViewDTO.class); + } + if (version != null && version.getScriptTrain() != null) { + Long trainId = version.getScriptTrain().getId(); + scriptService.runScriptTrain(trainId, submission); + } + } catch (IOException e) { + e.printStackTrace(); + } + return "redirect:/assignment/" + assignmentId; + } + /** * Adds and uploads a newly created submission. * @@ -129,13 +174,14 @@ public class SubmissionController { throw new DisallowedUploadException("One or more of the submitted files are not allowed"); Long id = submissionService.addSubmission(createSubmissionDto); + CoreSubmission submission = submissionService.getOrCreateSubmitCoreSubmission(id); - fileService.uploadFiles(request.getFileMap(), id, submissionsDirectory); + fileService.uploadFiles(request.getFileMap(), submission.getId(), submissionsDirectory); VersionViewDTO version = versionService.getVersionOfAssignmentForGroup(assignment.getId(), group.getId()); if (version != null && version.getScriptTrain() != null) { - scriptService.runScriptTrain(version.getScriptTrain().getId(), id); + scriptService.runScriptTrain(version.getScriptTrain().getId(), submission); } emailService.sendEmailToAuthPerson( @@ -192,7 +238,8 @@ public class SubmissionController { public ResponseEntity<Resource> downloadFiles(@PathVariable Long submissionId) throws Exception { String submissionsDirectory = storageDir + "/submissions"; String downloadsDirectory = tempStorageDir + "/downloads"; - Resource resource = fileService.downloadFiles(submissionId, submissionsDirectory, downloadsDirectory); + Resource resource = fileService.downloadFiles(submissionService.getSubmissionIdByCoreId(submissionId), + submissionsDirectory, downloadsDirectory); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE)) .header(HttpHeaders.CONTENT_DISPOSITION, @@ -343,7 +390,8 @@ public class SubmissionController { VersionViewDTO version = versionService.getVersionOfAssignmentForGroup(assignmentId, groupId); if (version != null && version.getScriptTrain() != null) { - scriptService.runScriptTrain(version.getScriptTrain().getId(), id); + scriptService.runScriptTrain(version.getScriptTrain().getId(), + submissionService.getOrCreateSubmitCoreSubmission(id)); } return "resubmitted"; diff --git a/src/main/java/nl/tudelft/submit/csv/CSVService.java b/src/main/java/nl/tudelft/submit/csv/CSVService.java index 642433207272449581d60126bd610c90b4d1d08c..b19933291e6a507a0f011ee65d3dbb1b8852498f 100644 --- a/src/main/java/nl/tudelft/submit/csv/CSVService.java +++ b/src/main/java/nl/tudelft/submit/csv/CSVService.java @@ -17,9 +17,7 @@ */ package nl.tudelft.submit.csv; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Reader; +import java.io.*; import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; diff --git a/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java b/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java index d9af50c9e67e9b572f7f3f2dd9913b148c20508c..921eafef4c9cc5df6917bef21f8976ab74dbe5e9 100644 --- a/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java +++ b/src/main/java/nl/tudelft/submit/dto/create/SubmitAssignmentCreateDTO.java @@ -24,10 +24,7 @@ import javax.validation.constraints.NotNull; import org.springframework.format.annotation.DateTimeFormat; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import lombok.*; import nl.tudelft.labracore.api.dto.Description; import nl.tudelft.labracore.api.dto.ModuleIdDTO; import nl.tudelft.labracore.lib.api.GradeScheme; @@ -63,6 +60,8 @@ public class SubmitAssignmentCreateDTO extends Create<SubmitAssignment> { private List<String> allowedFileTypes = null; + private Boolean hidden = false; + @NotNull private ModuleIdDTO module; diff --git a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java index bb2f96dc8c15684d6fe632c7a2cb9563ec27beb6..ba04c2fc0796ff429606c1190082b4f52a6f61e5 100644 --- a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java +++ b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentDetailsDTO.java @@ -57,6 +57,8 @@ public class SubmitAssignmentDetailsDTO { private ModuleLayer1DTO module; + private Boolean hidden; + private List<SubmissionSummaryDTO> submissions; } diff --git a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java index cebb589587f806b5a9fcd40726482adfa9ad7428..895e49df4d2d6e053fc54e0a0e19f91561933bd0 100644 --- a/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java +++ b/src/main/java/nl/tudelft/submit/dto/view/labracore/SubmitAssignmentSummaryDTO.java @@ -43,5 +43,6 @@ public class SubmitAssignmentSummaryDTO extends WithNoteViewDTO<AssignmentSummar private String approveScript; private Integer submissionLimit; private List<String> allowedFileTypes; + private Boolean hidden = true; } diff --git a/src/main/java/nl/tudelft/submit/enums/TokenType.java b/src/main/java/nl/tudelft/submit/enums/TokenType.java index 29ff4b2b3f0af560ef2285772bee55da95298a40..2c9ea4b9518fd4158faa7ef680ac1f7f24b7aaae 100644 --- a/src/main/java/nl/tudelft/submit/enums/TokenType.java +++ b/src/main/java/nl/tudelft/submit/enums/TokenType.java @@ -83,7 +83,7 @@ public enum TokenType { "abs|max|min|sqrt|avg|clamp" ), VARIABLE( - "[A-Za-z_][A-Za-z0-9_]*" + "[A-Za-z0-9_]*[A-Za-z_][A-Za-z0-9_]*" ); private Pattern pattern; diff --git a/src/main/java/nl/tudelft/submit/model/SubmitSubmission.java b/src/main/java/nl/tudelft/submit/model/AbstractSubmitSubmission.java similarity index 84% rename from src/main/java/nl/tudelft/submit/model/SubmitSubmission.java rename to src/main/java/nl/tudelft/submit/model/AbstractSubmitSubmission.java index d51c74dbcf5fcd1afa49f41d0957e64173628885..ad8ef14a30aa01b00712f91ce716fdae879931a4 100644 --- a/src/main/java/nl/tudelft/submit/model/SubmitSubmission.java +++ b/src/main/java/nl/tudelft/submit/model/AbstractSubmitSubmission.java @@ -20,22 +20,23 @@ package nl.tudelft.submit.model; import java.util.ArrayList; import java.util.List; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.OneToMany; +import javax.persistence.*; import javax.validation.constraints.NotNull; import lombok.*; +import lombok.experimental.SuperBuilder; import nl.tudelft.submit.model.script.ScriptTrainResult; @Data @Entity -@Builder +@SuperBuilder @NoArgsConstructor @AllArgsConstructor -public class SubmitSubmission { +@Inheritance(strategy = InheritanceType.JOINED) +public abstract class AbstractSubmitSubmission { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull diff --git a/src/main/java/nl/tudelft/submit/model/CoreSubmission.java b/src/main/java/nl/tudelft/submit/model/CoreSubmission.java new file mode 100644 index 0000000000000000000000000000000000000000..515764b5c23f21d7eff67882ee155a9d8ce99c04 --- /dev/null +++ b/src/main/java/nl/tudelft/submit/model/CoreSubmission.java @@ -0,0 +1,36 @@ +/* + * Submit + * Copyright (C) 2020 - Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.submit.model; + +import javax.persistence.Entity; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@Entity +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class CoreSubmission extends AbstractSubmitSubmission { + + private Long coreSubmissionId; + +} diff --git a/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java b/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java index 669dd8da5110a78b42ff9c4fa403d63e77a3d513..11371d4bb4f650b3d10f304faaca8a2e65626925 100644 --- a/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java +++ b/src/main/java/nl/tudelft/submit/model/SubmitAssignment.java @@ -20,6 +20,7 @@ package nl.tudelft.submit.model; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Lob; +import javax.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; @@ -42,4 +43,8 @@ public class SubmitAssignment { @Lob private String guidelines; + @NotNull + @Builder.Default + private Boolean hidden = true; + } diff --git a/src/main/java/nl/tudelft/submit/model/TestSubmission.java b/src/main/java/nl/tudelft/submit/model/TestSubmission.java new file mode 100644 index 0000000000000000000000000000000000000000..6188f40eccd31f0ab4ab4589dbecc10ddbd675d5 --- /dev/null +++ b/src/main/java/nl/tudelft/submit/model/TestSubmission.java @@ -0,0 +1,43 @@ +/* + * Submit + * Copyright (C) 2020 - Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.submit.model; + +import java.time.LocalDateTime; + +import javax.persistence.Entity; +import javax.validation.constraints.NotNull; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@Entity +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class TestSubmission extends AbstractSubmitSubmission { + + private Long assignmentId; + + @NotNull + @Builder.Default + private LocalDateTime createdDate = LocalDateTime.now(); + +} diff --git a/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java b/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java index f4b1678fa989f10204af04ec315adf96bac5166a..65e31bdf0c06b15515efddd7d8d525787ebf3753 100644 --- a/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java +++ b/src/main/java/nl/tudelft/submit/model/grading/GradingFormula.java @@ -20,6 +20,7 @@ package nl.tudelft.submit.model.grading; import java.io.Serializable; import javax.persistence.EmbeddedId; +import javax.persistence.Lob; import javax.persistence.MappedSuperclass; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @@ -44,6 +45,7 @@ public abstract class GradingFormula<E extends Serializable> { private E entityId; @NotBlank + @Lob private String formula; @NotNull diff --git a/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java b/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java index dae964779d37227a3f5f758d45415515836ef69b..94db51a63dfeb1f95b10c46b8f85e9fb37ea557b 100644 --- a/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java +++ b/src/main/java/nl/tudelft/submit/model/script/AbstractScriptResult.java @@ -23,7 +23,7 @@ import javax.validation.constraints.NotNull; import lombok.*; import lombok.experimental.SuperBuilder; import nl.tudelft.submit.enums.ScriptStatus; -import nl.tudelft.submit.model.SubmitSubmission; +import nl.tudelft.submit.model.AbstractSubmitSubmission; @Data @SuperBuilder @@ -38,7 +38,7 @@ public abstract class AbstractScriptResult { @NotNull @ManyToOne - private SubmitSubmission submission; + private AbstractSubmitSubmission submission; @NotNull @Builder.Default diff --git a/src/main/java/nl/tudelft/submit/repository/AbstractSubmitSubmissionRepository.java b/src/main/java/nl/tudelft/submit/repository/AbstractSubmitSubmissionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..b371c68e029d47d5c6bde81c894195433f1fdb65 --- /dev/null +++ b/src/main/java/nl/tudelft/submit/repository/AbstractSubmitSubmissionRepository.java @@ -0,0 +1,26 @@ +/* + * Submit + * Copyright (C) 2020 - Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.submit.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import nl.tudelft.submit.model.AbstractSubmitSubmission; + +public interface AbstractSubmitSubmissionRepository<T extends AbstractSubmitSubmission> + extends JpaRepository<T, Long> { +} diff --git a/src/main/java/nl/tudelft/submit/repository/CoreSubmissionRepository.java b/src/main/java/nl/tudelft/submit/repository/CoreSubmissionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..e64809e24c8ae0f9a49071f6214cc91e8d4ecf62 --- /dev/null +++ b/src/main/java/nl/tudelft/submit/repository/CoreSubmissionRepository.java @@ -0,0 +1,45 @@ +/* + * Submit + * Copyright (C) 2020 - Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.submit.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.stereotype.Repository; + +import nl.tudelft.submit.model.CoreSubmission; + +@Repository +public interface CoreSubmissionRepository extends JpaRepository<CoreSubmission, Long> { + + Optional<CoreSubmission> findByCoreSubmissionId(Long coreId); + + /** + * Finds the CoreSubmission by id or throws a ResourceNotFoundException. + * + * @param id the id of the submission + * @return the CoreSubmission object + */ + default CoreSubmission findByIdOrThrow(Long id) { + return findById(id) + .orElseThrow( + () -> new ResourceNotFoundException("SubmitCoreSubmission was not found: " + id)); + } + +} diff --git a/src/main/java/nl/tudelft/submit/repository/SubmitSubmissionRepository.java b/src/main/java/nl/tudelft/submit/repository/TestSubmissionRepository.java similarity index 68% rename from src/main/java/nl/tudelft/submit/repository/SubmitSubmissionRepository.java rename to src/main/java/nl/tudelft/submit/repository/TestSubmissionRepository.java index cfec29eccdac9913952aa1f75db15c0501fb58f1..e6d4593a19a55172d254c71d1c41bf70021ac6f5 100644 --- a/src/main/java/nl/tudelft/submit/repository/SubmitSubmissionRepository.java +++ b/src/main/java/nl/tudelft/submit/repository/TestSubmissionRepository.java @@ -17,24 +17,29 @@ */ package nl.tudelft.submit.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.stereotype.Repository; -import nl.tudelft.submit.model.SubmitSubmission; +import nl.tudelft.submit.model.TestSubmission; @Repository -public interface SubmitSubmissionRepository extends JpaRepository<SubmitSubmission, Long> { +public interface TestSubmissionRepository extends JpaRepository<TestSubmission, Long> { + + List<TestSubmission> findAllByAssignmentId(Long assignmentId); /** - * Finds the SubmitSubmission by id or throws a ResourceNotFoundException. + * Finds the TestSubmission by id or throws a ResourceNotFoundException. * * @param id the id of the submission - * @return the SubmitSubmission object + * @return the TestSubmission object */ - default SubmitSubmission findByIdOrThrow(Long id) { + default TestSubmission findByIdOrThrow(Long id) { return findById(id) - .orElseThrow(() -> new ResourceNotFoundException("SubmitSubmission was not found: " + id)); + .orElseThrow( + () -> new ResourceNotFoundException("SubmitTestSubmission was not found: " + id)); } } diff --git a/src/main/java/nl/tudelft/submit/security/AuthorizationService.java b/src/main/java/nl/tudelft/submit/security/AuthorizationService.java index 5d11d88b1a6f19b030fcd103c7770b2f68ca5323..e8731f9d1772a6f6c5ad8b19f1868fd8ce373169 100644 --- a/src/main/java/nl/tudelft/submit/security/AuthorizationService.java +++ b/src/main/java/nl/tudelft/submit/security/AuthorizationService.java @@ -44,6 +44,7 @@ import nl.tudelft.submit.repository.announcement.EditionAnnouncementRepository; import nl.tudelft.submit.repository.script.ScriptRepository; import nl.tudelft.submit.repository.script.ScriptTrainRepository; import nl.tudelft.submit.repository.script.ScriptWagonRepository; +import nl.tudelft.submit.service.AssignmentService; import nl.tudelft.submit.service.VersionService; @Service @@ -68,6 +69,8 @@ public class AuthorizationService { @Autowired private AssignmentCacheManager assignmentCache; @Autowired + private AssignmentService assignmentService; + @Autowired private SubmissionCacheManager submissionCache; @Autowired private StudentGroupCacheManager groupCache; @@ -830,7 +833,9 @@ public class AuthorizationService { * @return True iff the user can view the assignment */ public boolean canViewAssignment(Long assignmentId) { - return isAtLeastStudentInAssignment(assignmentId) || isTeacherROInAssignment(assignmentId); + boolean hidden = assignmentService.getSubmitAssignmentDetails(assignmentId).getHidden(); + return (isAtLeastStudentInAssignment(assignmentId) && !hidden) || canEditAssignment(assignmentId) + || isTeacherROInAssignment(assignmentId); } /** @@ -874,6 +879,16 @@ public class AuthorizationService { return isAtLeastStudentInAssignment(assignmentId) && isMemberOfGroup(groupId); } + /** + * Checks whether the authenticated user can make a test submission for an assignment. + * + * @param assignmentId The id of the assignment + * @return True iff the user can make a test submission for the assignment + */ + public boolean canSubmitTestSubmission(Long assignmentId) { + return isAtLeastHeadTAInAssignment(assignmentId); + } + /** * Checks whether the authenticated user can create an assignment in a module. * @@ -1109,12 +1124,22 @@ public class AuthorizationService { * Checks whether the authenticated user can create a group in a module. * * @param moduleId The id of the module - * @return True iff the user can view a group in the module. + * @return True iff the user can create a group in the module. */ public boolean canCreateGroup(Long moduleId) { return isAtLeastHeadTAInModule(moduleId); } + /** + * Checks whether the authenticated user can import groups of students. + * + * @param moduleId The id of the module + * @return True, iff the user can import groups of students. + */ + public boolean canImportStudentGroups(Long moduleId) { + return isAtLeastHeadTAInModule(moduleId); + } + /** * Checks whether the authenticated user can edit a group's note. * diff --git a/src/main/java/nl/tudelft/submit/service/AssignmentService.java b/src/main/java/nl/tudelft/submit/service/AssignmentService.java index 971bd68db54d13c27bcc1466515d7f47a517a3b2..6e5e69ca53a279c59b3860b2212ea43fb38ba4b9 100644 --- a/src/main/java/nl/tudelft/submit/service/AssignmentService.java +++ b/src/main/java/nl/tudelft/submit/service/AssignmentService.java @@ -25,6 +25,8 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; +import javax.transaction.Transactional; + import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -47,6 +49,7 @@ import nl.tudelft.submit.dto.patch.SubmitAssignmentPatchDTO; import nl.tudelft.submit.dto.patch.grading.AssignmentGradeImportDTO; import nl.tudelft.submit.dto.view.VersionViewDTO; import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO; +import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentSummaryDTO; import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO; import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO; import nl.tudelft.submit.dto.view.script.ScriptTrainResultViewDTO; @@ -54,6 +57,7 @@ import nl.tudelft.submit.model.SubmitAssignment; import nl.tudelft.submit.model.note.AssignmentNote; import nl.tudelft.submit.repository.SubmitAssignmentRepository; import nl.tudelft.submit.repository.note.AssignmentNoteRepository; +import nl.tudelft.submit.security.AuthorizationService; @Service public class AssignmentService { @@ -74,6 +78,9 @@ public class AssignmentService { @Autowired private SubmitAssignmentRepository assignmentRepository; @Autowired + @Lazy + private AuthorizationService authorizationService; + @Autowired private AssignmentNoteRepository noteRepository; @Autowired @@ -137,6 +144,7 @@ public class AssignmentService { SubmitAssignmentDetailsDTO.class); details.setScoreType(getOrCreateSubmitAssignment(id).getScoreType()); details.setGuidelines(getOrCreateSubmitAssignment(id).getGuidelines()); + details.setHidden(getOrCreateSubmitAssignment(id).getHidden()); return details; } @@ -309,7 +317,8 @@ public class AssignmentService { s.setIsLatest(false); s.setFeedback(feedbackService.getAllFeedbackPerSubmission(s.getId())); s.setScriptResults( - View.convert(submissionService.getOrCreateSubmitSubmission(s.getId()).getScriptResults(), + View.convert( + submissionService.getOrCreateSubmitCoreSubmission(s.getId()).getScriptResults(), ScriptTrainResultViewDTO.class)); byGroup.get(s.getGroup().getId()).add(s); }); @@ -335,7 +344,8 @@ public class AssignmentService { submissions.forEach(s -> { s.setFeedback(feedbackService.getAllFeedbackPerSubmission(s.getId())); s.setScriptResults( - View.convert(submissionService.getOrCreateSubmitSubmission(s.getId()).getScriptResults(), + View.convert( + submissionService.getOrCreateSubmitCoreSubmission(s.getId()).getScriptResults(), ScriptTrainResultViewDTO.class)); }); return submissions; @@ -405,4 +415,26 @@ public class AssignmentService { return files != null && files.length > 0; } + /** + * Toggles the hidden state of an assignment. + * + * @param id The id of the assignment. + */ + @Transactional + public void toggleHide(Long id) { + SubmitAssignment assignment = getOrCreateSubmitAssignment(id); + assignment.setHidden(!assignment.getHidden()); + assignmentRepository.save(assignment); + } + + /** + * Gets/Filters the assignments that are visible to the user + * + * @param assignments the assignments to filter on + * @return the visible/filtered assignments + */ + public List<SubmitAssignmentSummaryDTO> getVisibleAssignments( + List<SubmitAssignmentSummaryDTO> assignments) { + return assignments.stream().filter(x -> authorizationService.canViewAssignment(x.getId())).toList(); + } } diff --git a/src/main/java/nl/tudelft/submit/service/EditionService.java b/src/main/java/nl/tudelft/submit/service/EditionService.java index a6662baaf32b81851902d267a2e7b12871753f01..1fdf2e368a50097f64cb633ce90c3adb79f5f225 100644 --- a/src/main/java/nl/tudelft/submit/service/EditionService.java +++ b/src/main/java/nl/tudelft/submit/service/EditionService.java @@ -473,6 +473,7 @@ public class EditionService { * * @param id The id of the edition */ + @Transactional public void toggleHide(Long id) { SubmitEdition edition = getOrCreateSubmitEdition(id); edition.setHidden(!edition.getHidden()); diff --git a/src/main/java/nl/tudelft/submit/service/GradeService.java b/src/main/java/nl/tudelft/submit/service/GradeService.java index fb39593d3c82c9e6ebf64867e55d5cd460a315aa..7e1a6f740261fb699b2bb5948796b9d86bc728d9 100644 --- a/src/main/java/nl/tudelft/submit/service/GradeService.java +++ b/src/main/java/nl/tudelft/submit/service/GradeService.java @@ -30,7 +30,6 @@ import java.util.stream.Collectors; import javax.transaction.Transactional; -import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; @@ -72,8 +71,6 @@ public class GradeService { GRADE_FORMAT_100.setRoundingMode(RoundingMode.HALF_UP); } - private ModelMapper mapper = new ModelMapper(); - @Value("${labrador.api.url}") private String domain; @@ -118,6 +115,8 @@ public class GradeService { @Autowired private AssignmentService assignmentService; @Autowired + private SubmissionService submissionService; + @Autowired private UserSettingsService userSettingsService; @Autowired private NotificationService notificationService; @@ -804,7 +803,8 @@ public class GradeService { } gradeId = addGrade(create.getGradeCreateDTO(gradeType)); - updateGradesAfterGradedSubmission(create.getSubmissionId()); + updateGradesAfterGradedSubmission( + submissionService.getSubmissionIdByCoreId(create.getSubmissionId())); } feedbackService.addFeedback(create.getFeedbackCreateDTO(gradeId)); } diff --git a/src/main/java/nl/tudelft/submit/service/ScriptService.java b/src/main/java/nl/tudelft/submit/service/ScriptService.java index 0920565025b873fa07afd7b7e76fbac0f3de0d2a..86697d2f0adffca56498c0ed833dd63230af3dea 100644 --- a/src/main/java/nl/tudelft/submit/service/ScriptService.java +++ b/src/main/java/nl/tudelft/submit/service/ScriptService.java @@ -62,7 +62,8 @@ import nl.tudelft.submit.dto.patch.script.ScriptWagonPatchDTO; import nl.tudelft.submit.dto.view.script.ScriptWagonViewDTO; import nl.tudelft.submit.enums.NotificationPreference; import nl.tudelft.submit.enums.ScriptStatus; -import nl.tudelft.submit.model.SubmitSubmission; +import nl.tudelft.submit.model.AbstractSubmitSubmission; +import nl.tudelft.submit.model.CoreSubmission; import nl.tudelft.submit.model.Version; import nl.tudelft.submit.model.script.*; import nl.tudelft.submit.repository.SubmissionKeyRepository; @@ -545,15 +546,13 @@ public class ScriptService { /** * Runs the script train for a submission. * - * @param trainId The id of the script train - * @param submissionId The id of the submission + * @param trainId The id of the script train + * @param submission The submission to run the script on */ @Transactional - public void runScriptTrain(Long trainId, Long submissionId) { - submissionService.setPendingState(submissionId, true); - + public void runScriptTrain(Long trainId, AbstractSubmitSubmission submission) { + submissionService.setPendingState(submission.getId(), true); ScriptTrain train = getScriptTrainById(trainId); - SubmitSubmission submission = submissionService.getOrCreateSubmitSubmission(submissionId); scriptTrainResultRepository.save(ScriptTrainResult.builder().train(train).submission(submission) .status(ScriptStatus.RUNNING).build()); @@ -578,15 +577,13 @@ public class ScriptService { * @param submission The submission */ @Transactional - public void runScriptWagon(ScriptWagon wagon, SubmitSubmission submission) { + public void runScriptWagon(ScriptWagon wagon, AbstractSubmitSubmission submission) { ScriptWagonResult result = scriptWagonResultRepository.findByWagonIdAndSubmissionId(wagon.getId(), submission.getId()); result.setStatus(ScriptStatus.RUNNING); scriptWagonResultRepository.save(result); - Iterator<Script> scripts = wagon.getScripts().iterator(); - while (scripts.hasNext()) { - Script script = scripts.next(); + for (Script script : wagon.getScripts()) { runScript(script, submission); } } @@ -599,7 +596,7 @@ public class ScriptService { */ @Async @Transactional - public CompletableFuture<Void> runScript(Script script, SubmitSubmission submission) { + public CompletableFuture<Void> runScript(Script script, AbstractSubmitSubmission submission) { ScriptResult result = scriptResultRepository.findByScriptIdAndSubmissionId(script.getId(), submission.getId()); result.setStatus(ScriptStatus.RUNNING); @@ -625,7 +622,7 @@ public class ScriptService { * @param wagonId The id of the wagon * @param submission The submission */ - private void checkAndUpdateWagon(Long wagonId, SubmitSubmission submission) { + private void checkAndUpdateWagon(Long wagonId, AbstractSubmitSubmission submission) { ScriptWagonResult result = scriptWagonResultRepository.findByWagonIdAndSubmissionId(wagonId, submission.getId()); List<ScriptResult> scriptResults = scriptResultRepository @@ -692,13 +689,17 @@ public class ScriptService { calculateTrainResult(result, wagonResults); submissionService.setPendingState(submissionId, false); - gradeService.addGrade(new GradeCreateDTO() - .scheme(GradeCreateDTO.SchemeEnum.valueOf(result.getTrain().getGradeScheme().name())) - .score(result.getScore()) - .gradedAt(LocalDateTime.now()) - .isScriptGraded(true) - .submission(new SubmissionIdDTO().id(submissionId))); - gradeService.updateGradesAfterGradedSubmission(submissionId); + Optional<CoreSubmission> submission = submissionService.getCoreSubBySubmitSubId(submissionId); + if (submission.isPresent()) { + Long coreSubmissionId = submission.get().getCoreSubmissionId(); + gradeService.addGrade(new GradeCreateDTO() + .scheme(GradeCreateDTO.SchemeEnum.valueOf(result.getTrain().getGradeScheme().name())) + .score(result.getScore()) + .gradedAt(LocalDateTime.now()) + .isScriptGraded(true) + .submission(new SubmissionIdDTO().id(coreSubmissionId))); + gradeService.updateGradesAfterGradedSubmission(coreSubmissionId); + } } } @@ -782,7 +783,7 @@ public class ScriptService { .result(result).build()); scriptResultRepository.save(result); checkAndUpdateWagon(result.getScript().getWagon().getId(), - submissionService.getOrCreateSubmitSubmission(submissionId)); + submissionService.getOrCreateSubmitCoreSubmission(submissionId)); } } @@ -820,7 +821,7 @@ public class ScriptService { if (result.getStatus().isFinished()) { checkAndUpdateWagon(result.getScript().getWagon().getId(), - submissionService.getOrCreateSubmitSubmission(feedback.getSubmissionId())); + submissionService.getSubmitSubmissionById(feedback.getSubmissionId())); } } diff --git a/src/main/java/nl/tudelft/submit/service/StudentGroupService.java b/src/main/java/nl/tudelft/submit/service/StudentGroupService.java index b9b49438bd5bc39166090ca7a22d837f5fbccaa0..6c15fe42efeef2c2517a5ef75c7dafe0a02cef86 100644 --- a/src/main/java/nl/tudelft/submit/service/StudentGroupService.java +++ b/src/main/java/nl/tudelft/submit/service/StudentGroupService.java @@ -20,6 +20,9 @@ package nl.tudelft.submit.service; import static nl.tudelft.submit.enums.SwitchAction.JOIN; import static nl.tudelft.submit.enums.SwitchAction.LEAVE; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -30,13 +33,17 @@ import javax.transaction.Transactional; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.reactive.function.client.WebClientResponseException; +import com.opencsv.exceptions.CsvValidationException; + import nl.tudelft.labracore.api.StudentGroupControllerApi; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.librador.dto.view.View; import nl.tudelft.submit.cache.PersonCacheManager; import nl.tudelft.submit.cache.StudentGroupCacheManager; +import nl.tudelft.submit.csv.CSVService; import nl.tudelft.submit.dto.id.GroupId; import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO; import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO; @@ -47,7 +54,6 @@ import nl.tudelft.submit.model.Version; import nl.tudelft.submit.repository.SubmitGroupRepository; import nl.tudelft.submit.repository.SwitchEventRepository; import nl.tudelft.submit.repository.note.GroupNoteRepository; -import nl.tudelft.submit.repository.note.SubmissionNoteRepository; import reactor.core.publisher.Mono; @Service @@ -67,17 +73,16 @@ public class StudentGroupService { private SwitchEventRepository switchEventRepository; @Autowired private GroupNoteRepository groupNoteRepository; - @Autowired - private SubmissionNoteRepository submissionNoteRepository; @Autowired private ModuleDivisionService moduleDivisionService; @Autowired - private AssignmentService assignmentService; + private SubmissionService submissionService; @Autowired - private FeedbackService feedbackService; + private ModuleService moduleService; + @Autowired - private SubmissionService submissionService; + private CSVService csvService; private ModelMapper mapper = new ModelMapper(); @@ -171,13 +176,22 @@ public class StudentGroupService { } /** - * Creates a student group. + * Creates a student group. Throws GroupSwitchingException if a member is already in a group * * @param create The dto to create the group * @return The created group's id */ @Transactional public Long createGroup(StudentGroupCreateDTO create) { + if (create.getMembers() != null && !create.getMembers().isEmpty()) { + List<String> usernames = create.getMembers().stream().map(x -> x.getId().getPersonId()) + .map(id -> personCache.get(id).orElseThrow().getUsername()).toList(); + Long moduleId = create.getModule().getId(); + + // will throw GroupSwitchingException if a member is already in a group + arePeopleAlreadyInAGroup(moduleId, usernames, "Group cannot be created"); + } + Long id = groupApi.addGroup(create).block(); groupRepository.save(SubmitGroup.builder().id(id).build()); return id; @@ -213,9 +227,34 @@ public class StudentGroupService { groupApi.allocateGroupsForModule(capacity, moduleId).block(); } + /** + * Imports student groups from a csv file. Group members must already be a part of the edition. + * + * @param moduleId the id of the module to import for + * @param file the csv file + */ + @Transactional + public void importStudentGroups(Long moduleId, MultipartFile file) + throws IOException, CsvValidationException { + Map<String, List<String>> groupAssignment = new HashMap<>(); + csvService.readCSV(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8), false, 1) + .forEach(line -> groupAssignment + .computeIfAbsent(line[1], key -> new ArrayList<>()) + .add(line[0])); + + for (Map.Entry<String, List<String>> group : groupAssignment.entrySet()) { + createGroup(new StudentGroupCreateDTO().members(group.getValue().stream() + .map(x -> new RoleIdDTO() + .id(new Id().editionId(moduleService.getModuleById(moduleId).getEdition().getId()) + .personId(personCache.get(x).getId()))) + .toList()).name(group.getKey()) + .module(new ModuleIdDTO().id(moduleId)).capacity(group.getValue().size())); + } + } + /** * Attempts to add a person to a group and, if successful, creates a switching event if the group is - * locked. + * locked. Throws GroupSwitchingException if is already in a group in the module. * * @param personId the id of the role of the user in the edition * @param groupId the id of the group to join @@ -223,6 +262,11 @@ public class StudentGroupService { */ @Transactional public Long addPersonToGroup(Long personId, Long groupId) { + Long moduleId = getGroupDetails(groupId).getModule().getId(); + + // will throw a GroupSwitchingException if person is already in a group + checkIfPersonIsAlreadyInAGroup(personId, moduleId, "Cannot add person to group"); + groupApi.addMemberToGroup(groupId, personId).block(); if (groupApi.isGroupLockedAndRoleStudent(personId, groupId).block()) { @@ -233,12 +277,19 @@ public class StudentGroupService { } /** - * Adds multiple people to a group if the group is not yet locked. + * Adds multiple people to a group if the group is not yet locked. Throws GroupSwitchingException if at + * least one person in the group is already in a group. * * @param id the id of the group * @param usernames the list of usernames of the people to be added */ - public void addPeopleToGroup(Long id, List<String> usernames) { + @Transactional + public void addPeopleToGroup(Long id, List<String> usernames) throws GroupSwitchingException { + Long moduleId = getGroupDetails(id).getModule().getId(); + + // Will throw GroupSwitchingException if at least one of the people are already in a group + arePeopleAlreadyInAGroup(moduleId, usernames, "Cannot add person to group"); + if (groupApi.isGroupLocked(id).block()) { if (usernames.size() == 1) { PersonSummaryDTO person = personCache.get(usernames.get(0)); @@ -248,10 +299,45 @@ public class StudentGroupService { throw new GroupSwitchingException("Cannot import multiple people into locked group."); } } - groupApi.addGroupMembers(id, usernames).block(); } + /** + * Checks if a person is already in a group in a given module. Throws GroupSwitchingException if there is + * at least one person that is already in a group + * + * @param personId the id of the person + * @param moduleId the id of the module + */ + private void checkIfPersonIsAlreadyInAGroup(Long personId, Long moduleId, String message) + throws GroupSwitchingException { + Optional<StudentGroupDetailsDTO> currentGroup = getGroupForPersonInModule(personId, moduleId); + + // person is not in any other groups in the module + if (currentGroup.isEmpty()) { + return; + } + + String personName = personCache.get(personId).orElseThrow().getDisplayName(); + String currentGroupName = currentGroup.get().getName(); + throw new GroupSwitchingException( + message + " because " + personName + " is already in " + currentGroupName); + } + + /** + * Checks if at least one person in the given list is already a part of a group in the module + * + * @param usernames list of usernames to be checked + * @param moduleId the id of the module + * + */ + private void arePeopleAlreadyInAGroup(Long moduleId, List<String> usernames, String message) { + for (String name : usernames) { + Long id = personCache.get(name).getId(); + checkIfPersonIsAlreadyInAGroup(id, moduleId, message); + } + } + /** * Attempts to remove a person from a group and, if successful, creates a switching event if the group is * locked. diff --git a/src/main/java/nl/tudelft/submit/service/SubmissionService.java b/src/main/java/nl/tudelft/submit/service/SubmissionService.java index f736e49752940abe55de67def2f028503b321914..7ee247d156d9461402e1d8dad5224fcca7b1f2f0 100644 --- a/src/main/java/nl/tudelft/submit/service/SubmissionService.java +++ b/src/main/java/nl/tudelft/submit/service/SubmissionService.java @@ -50,9 +50,13 @@ import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentSummaryDTO; import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO; import nl.tudelft.submit.dto.view.script.ScriptTrainResultViewDTO; import nl.tudelft.submit.enums.SubmissionDownloadPreference; +import nl.tudelft.submit.model.AbstractSubmitSubmission; +import nl.tudelft.submit.model.CoreSubmission; import nl.tudelft.submit.model.SubmitAssignment; -import nl.tudelft.submit.model.SubmitSubmission; -import nl.tudelft.submit.repository.SubmitSubmissionRepository; +import nl.tudelft.submit.model.TestSubmission; +import nl.tudelft.submit.repository.AbstractSubmitSubmissionRepository; +import nl.tudelft.submit.repository.CoreSubmissionRepository; +import nl.tudelft.submit.repository.TestSubmissionRepository; import nl.tudelft.submit.repository.note.SubmissionNoteRepository; @Service @@ -77,9 +81,13 @@ public class SubmissionService { private SubmissionControllerApi submissionApi; @Autowired - private SubmitSubmissionRepository submissionRepository; + private CoreSubmissionRepository submissionRepository; + @Autowired + private TestSubmissionRepository testSubmissionRepository; @Autowired private SubmissionNoteRepository submissionNoteRepository; + @Autowired + private AbstractSubmitSubmissionRepository<AbstractSubmitSubmission> abstractSubmitSubmissionRepository; @Autowired @Lazy @@ -123,7 +131,7 @@ public class SubmissionService { @Transactional public Long addSubmission(SubmissionCreateDTO createDTO) { Long id = submissionApi.addSubmission(createDTO).block(); - submissionRepository.saveAndFlush(SubmitSubmission.builder().id(id).build()); + submissionRepository.saveAndFlush(CoreSubmission.builder().coreSubmissionId(id).build()); return id; } @@ -405,7 +413,8 @@ public class SubmissionService { String downloadsDirectory = tempStorageDir + "/downloads"; String submissionsDirectory = storageDir + "/submissions"; if (submissionIds.size() == 1) { - return fileService.downloadFiles(submissionIds.get(0), submissionsDirectory, downloadsDirectory); + return fileService.downloadFiles(getSubmissionIdByCoreId(submissionIds.get(0)), + submissionsDirectory, downloadsDirectory); } List<SubmissionDetailsDTO> submissions = submissionCache.get(submissionIds); File customDownloadDir = new File(downloadsDirectory + File.separator + "custom_download"); @@ -444,15 +453,16 @@ public class SubmissionService { } private void downloadSubmission(String downloadPath, SubmissionDetailsDTO submission) throws IOException { + Long submitSubmissionId = getSubmissionIdByCoreId(submission.getId()); String submissionsDirectory = storageDir + "/submissions"; - Path path = Paths.get(downloadPath + File.separator + "submission_" + submission.getId()); + Path path = Paths.get(downloadPath + File.separator + "submission_" + submitSubmissionId); Files.createDirectory(path); File submissionDir = new File(submissionsDirectory + File.separator - + StringUtils.cleanPath(fileService.getDirectory(submission.getId()))); + + StringUtils.cleanPath(fileService.getDirectory(submitSubmissionId))); File[] files = submissionDir.listFiles(); for (File file : files) { Files.copy(file.toPath(), Paths.get(downloadPath + File.separator + "submission_" - + submission.getId() + File.separator + file.getName())); + + submitSubmissionId + File.separator + file.getName())); } } @@ -476,6 +486,7 @@ public class SubmissionService { .getOrCreateSubmitAssignment(a.getId()); assignment.setScoreType(findAssignment.getScoreType()); assignment.setGuidelines(findAssignment.getGuidelines()); + assignment.setHidden(findAssignment.getHidden()); return assignment; }, a -> getSubmitSubmissions(a.getId(), groupId)))); return assignments; @@ -498,10 +509,10 @@ public class SubmissionService { view.setIsLatest(false); view.setFeedback(feedbackService.getAllFeedbackPerSubmission(s.getId())); view.setNote(submissionNoteRepository.getByEntityId(new SubmissionId(view.getId()))); - SubmitSubmission submitSubmission = getOrCreateSubmitSubmission(s.getId()); - view.setScriptResults(View.convert(submitSubmission.getScriptResults(), + CoreSubmission coreSubmission = getOrCreateSubmitCoreSubmission(s.getId()); + view.setScriptResults(View.convert(coreSubmission.getScriptResults(), ScriptTrainResultViewDTO.class)); - view.setScriptPending(submitSubmission.getScriptPending()); + view.setScriptPending(coreSubmission.getScriptPending()); return view; }).collect(Collectors.toList()); if (!submissions.isEmpty()) { @@ -511,15 +522,16 @@ public class SubmissionService { } /** - * Gets the submit submission with the corresponding id or creates one. + * Gets the submit core submission with the corresponding id or creates one. * * @param submissionId The id of the submission - * @return The submit submission + * @return The submit core submission */ @Transactional - public SubmitSubmission getOrCreateSubmitSubmission(Long submissionId) { - return submissionRepository.findById(submissionId).orElseGet( - () -> submissionRepository.save(SubmitSubmission.builder().id(submissionId).build())); + public CoreSubmission getOrCreateSubmitCoreSubmission(Long submissionId) { + return submissionRepository.findByCoreSubmissionId(submissionId).orElseGet( + () -> submissionRepository + .save(CoreSubmission.builder().coreSubmissionId(submissionId).build())); } /** @@ -530,10 +542,60 @@ public class SubmissionService { */ @Transactional public void setPendingState(Long submissionId, boolean pending) { - submissionRepository.findById(submissionId).ifPresent(s -> { + abstractSubmitSubmissionRepository.findById(submissionId).ifPresent(s -> { s.setScriptPending(pending); - submissionRepository.save(s); + abstractSubmitSubmissionRepository.save(s); }); } + /** + * Get SubmitSubmission by id + * + * @param id The id of the submission + * @return The SubmitSubmission + */ + public AbstractSubmitSubmission getSubmitSubmissionById(Long id) { + return abstractSubmitSubmissionRepository.findById(id).get(); + } + + /** + * Get SubmitSubmissionId by core submissionId + * + * @param coreSubmissionId The core id of the submission + * @return the SubmitSubmissionId + */ + public Long getSubmissionIdByCoreId(Long coreSubmissionId) { + return submissionRepository.findByCoreSubmissionId(coreSubmissionId).get().getId(); + } + + /** + * Get core submission by SubmitSubmissionId + * + * @param submissionId the id of the submission + * @return the core submission (if any) + */ + public Optional<CoreSubmission> getCoreSubBySubmitSubId(Long submissionId) { + return submissionRepository.findById(submissionId); + } + + /** + * Creates a new test submission for an assignment + * + * @param assignmentId the assignment id + * @return the created test submission + */ + public TestSubmission createTestSubmissionForAssignment(Long assignmentId) { + return testSubmissionRepository.save(TestSubmission.builder().assignmentId(assignmentId).build()); + } + + /** + * Returns the latest test submission for an assignment + * + * @param assignmentId the assignment id + * @return the latest test submission (if any) + */ + public Optional<TestSubmission> getLatestTestSubmissionByAssignment(Long assignmentId) { + return testSubmissionRepository.findAllByAssignmentId(assignmentId).stream() + .max(java.util.Comparator.comparing(TestSubmission::getCreatedDate)); + } } diff --git a/src/main/java/nl/tudelft/submit/startup/PublishAllAssignmentsService.java b/src/main/java/nl/tudelft/submit/startup/PublishAllAssignmentsService.java new file mode 100644 index 0000000000000000000000000000000000000000..7896ca8c7dbd29ea7b6f788b3d6353d9643fc534 --- /dev/null +++ b/src/main/java/nl/tudelft/submit/startup/PublishAllAssignmentsService.java @@ -0,0 +1,46 @@ +/* + * Submit + * Copyright (C) 2020 - Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.submit.startup; + +import javax.transaction.Transactional; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import lombok.AllArgsConstructor; +import nl.tudelft.submit.repository.SubmitAssignmentRepository; + +/** + * A service that runs during startup. This startup procedure goes through all assignments and publishes them + * in Submit. + */ +@Service +@AllArgsConstructor +@ConditionalOnExpression("#{'${submit.startup.publish-all-assignments}' == 'true'}") +public class PublishAllAssignmentsService { + + private final SubmitAssignmentRepository submitAssignmentRepository; + + @Transactional + @EventListener(ApplicationReadyEvent.class) + public void publishAllAssignments() { + submitAssignmentRepository.findAll().forEach(assignment -> assignment.setHidden(false)); + } +} diff --git a/src/main/resources/application.template.yaml b/src/main/resources/application.template.yaml index 4a0d91e0dd77c36a9fbe0c9f38cbdac3f7eb159b..e18e6b1c927613155de43c22a1dcfdc1e584c663 100644 --- a/src/main/resources/application.template.yaml +++ b/src/main/resources/application.template.yaml @@ -34,6 +34,8 @@ submit: timeout: 1800 # in seconds cache: person-timeout: 60 # in seconds + startup: + publish-all-assignments: false spring: profiles: diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index fc079dc99b5c6d125777f6fa236a551df62f8d6a..bdf520f8d64edc037421b6c624405cd1ab7e35b6 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -140,6 +140,7 @@ module.no_group = No group module.progress = Progress module.student_search = Username / Group / Student number / Student name module.create_groups = Create groups +module.create_groups.import = Import groups module.create_groups.allocate = Allocate module.create_groups.generate = Generate module.create_groups.amount = Amount of groups @@ -147,6 +148,11 @@ module.create_groups.amount.enter = Enter amount module.export_groups = Export groups assignment.add = Add assignment +assignment.show = Show assignment +assignment.hide = Hide assignment +assignment.hide.info = This assignment is hidden. It cannot be seen by students. If this assignment should not be hidden, you can make it visible by pressing 'Show assignment' below. +assignment.draft = Make draft +assignment.draft.help = By making an assignment draft, only instructors and head TAs will be able to see it. assignment.download_file = Download assignment file assignment.file.help = Upload any assignment file e.g. assignment description. You can delete it later using the trash button. You can use a .zip file to upload multiple files. assignment.file.deleted = File deleted @@ -154,6 +160,7 @@ assignment.edit = Edit assignment assignment.submission = Submission assignment.submissions = Submissions assignment.submit = Submit +assignment.test_submission = Test submission assignment.no_submissions = No submissions assignment.no_submission = No submission assignment.versions = Versions diff --git a/src/main/resources/migrations.yaml b/src/main/resources/migrations.yaml index 36192c0846c16ffa1e9d35734f53ec0c087f529f..80014e8cad3868cea4a529365d30d1cd3b6f5e92 100644 --- a/src/main/resources/migrations.yaml +++ b/src/main/resources/migrations.yaml @@ -538,4 +538,212 @@ databaseChangeLog: - column: name: guidelines type: CLOB(32768) - tableName: submit_assignment \ No newline at end of file + tableName: submit_assignment + - changeSet: + id: 1701359245733-1 + author: dsav (generated) + changes: + - modifyDataType: + columnName: formula + newDataType: CLOB(327682) + tableName: edition_grading_formula + - changeSet: + id: 1701359245733-2 + author: dsav (generated) + changes: + - modifyDataType: + columnName: formula + newDataType: CLOB(327683) + tableName: module_grading_formula + - changeSet: + id: 1700042030918-1 + author: dsav (generated) + changes: + - addColumn: + columns: + - column: + constraints: + nullable: false + name: hidden + type: BOOLEAN + defaultValueBoolean: true + tableName: submit_assignment + - changeSet: + id: 1701169247806-2 + author: dsav (generated) + changes: + - createTable: + columns: + - column: + name: core_submission_id + type: BIGINT + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_B + name: id + type: BIGINT + tableName: core_submission + - changeSet: + id: 1701169247806-1 + author: dsav (generated) + changes: + - createTable: + columns: + - column: + autoIncrement: true + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_8 + name: id + type: BIGINT + - column: + constraints: + nullable: false + name: script_pending + type: BIT + tableName: abstract_submit_submission + - changeSet: + id: 1701169247806-3 + author: dsav (generated) + changes: + - createTable: + columns: + - column: + name: assignment_id + type: BIGINT + - column: + constraints: + nullable: false + name: created_date + type: TIMESTAMP + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_3A + name: id + type: BIGINT + tableName: test_submission + - changeSet: + id: 1701169247806-4 + author: dsav (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: id + baseTableName: test_submission + constraintName: FK1awpk7v5us32q8e1u0s4ph46f + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: abstract_submit_submission + validate: true + - changeSet: + id: 1701169247806-5 + author: dsav (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: submission_id + baseTableName: script_result + constraintName: FK34g7ed9l54iae3ef1w7vub3f + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: abstract_submit_submission + validate: true + - changeSet: + id: 1701169247806-6 + author: dsav (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: id + baseTableName: core_submission + constraintName: FK7eui7k6r612cxrl5dsf84ovjd + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: abstract_submit_submission + validate: true + - changeSet: + id: 1701169247806-7 + author: dsav (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: submission_id + baseTableName: script_wagon_result + constraintName: FKcdrsre034hq6mamnf9frthdu9 + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: abstract_submit_submission + validate: true + - changeSet: + id: 1701169247806-8 + author: dsav (generated) + changes: + - addForeignKeyConstraint: + baseColumnNames: submission_id + baseTableName: script_train_result + constraintName: FKkgdkislnb6718bjhkx7nokoy7 + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: abstract_submit_submission + validate: true + - changeSet: + id: 1701169247806-9 + author: dsav (generated) + changes: + - dropForeignKeyConstraint: + baseTableName: script_train_result + constraintName: FKc3qhjvo3u0bnidlhtjc7xw3f8 + - changeSet: + id: 1701169247806-10 + author: dsav (generated) + changes: + - dropForeignKeyConstraint: + baseTableName: script_result + constraintName: FKtatgidyimgtoja7t71v22cuny + - changeSet: + id: 1701169247806-11 + author: dsav (generated) + changes: + - dropForeignKeyConstraint: + baseTableName: script_wagon_result + constraintName: FKtj6vpvtpy35jrp4183d9btl7i + - changeSet: + id: 1701169247806-12 + author: dsav + changes: + - sql: + comment: Move data to 'abstract_submit_submission'. + sql: insert into abstract_submit_submission (script_pending) + select s.script_pending from submit_submission as s; + - changeSet: + id: 1701169247806-13 + author: dsav + changes: + - sql: + comment: Move data to 'core_submission'. + sql: insert into core_submission (core_submission_id, id) + select s.id, s.id from submit_submission as s; + - changeSet: + id: 1701169247806-14 + author: dsav + changes: + - sql: + comment: Delete and drop submit_submission table. + sql: delete from submit_submission; + drop table submit_submission; \ No newline at end of file diff --git a/src/main/resources/templates/assignment/add.html b/src/main/resources/templates/assignment/add.html index 9100b59fa862435834c943218fc2d08968e92072..e407c5d1522dd9dd2cefe713d6ce4b931e5421eb 100644 --- a/src/main/resources/templates/assignment/add.html +++ b/src/main/resources/templates/assignment/add.html @@ -139,6 +139,18 @@ type="checkbox" /> <label for="assignment-accept-late" th:text="#{general.allow}"></label> </div> + <div> + <label for="make-draft" th:text="#{assignment.draft}"></label> + <div class="tooltip"> + <span class="tooltip__control fa-solid fa-question"></span> + <div role="tooltip" style="white-space: initial; min-width: 20rem"> + <p th:text="#{assignment.draft.help}"></p> + </div> + </div> + </div> + <div class="checkbox"> + <input id="make-draft" type="checkbox" value="true" th:name="hidden" /> + </div> <div class="underlined" style="grid-column: span 2"></div> diff --git a/src/main/resources/templates/assignment/test_submission.html b/src/main/resources/templates/assignment/test_submission.html new file mode 100644 index 0000000000000000000000000000000000000000..19312a8e285b408b17e9a2bebafbc4f52b8b62f3 --- /dev/null +++ b/src/main/resources/templates/assignment/test_submission.html @@ -0,0 +1,140 @@ +<!-- + + Submit + Copyright (C) 2020 - Delft University of Technology + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +--> +<!DOCTYPE html> +<html + lang="en" + xmlns:th="http://www.thymeleaf.org" + xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> + <body> + <dialog + th:fragment="overlay" + th:if="${@authorizationService.canSubmitTestSubmission(assignment.id)}" + th:id="|test-submission-${assignment.id}-overlay|" + class="dialog"> + <form + th:action="@{|/submission/test-submission/${assignment.id}|}" + method="post" + class="flex vertical p-7" + enctype="multipart/form-data"> + <h1 class="underlined font-500" th:text="#{assignment.submit}"></h1> + + <div class="grid col-2 align-center gap-3" style="--col-1: minmax(0, 8rem)"> + <input th:name="assignment.id" th:value="${assignment.id}" type="hidden" /> + + <span + th:unless="${versions.isEmpty() or versions[0].name == '_unit'}" + th:text="#{assignment.versions}"></span> + <div + th:unless="${versions.isEmpty() or versions[0].name == '_unit'}" + class="form-hstack-grid"> + <div class="checkbox" th:each="version: ${versions}"> + <input + th:id="|version-group-${version.id}|" + th:value="${version.id}" + th:name="versionId" + type="radio" /> + <label + th:for="|version-group-${version.id}|" + th:text="${version.name}"></label> + </div> + </div> + + <label for="file" th:text="#{general.file}"></label> + <input + type="file" + id="file" + name="file" + th:accept="${assignment.allowedFileTypes == null} ? '*' : ${#strings.substring(assignment.allowedFileTypes.toString(), 1, #strings.length(assignment.allowedFileTypes.toString())-1)}" + onchange="readColumnOptions()" + required /> + </div> + + <div class="flex space-between"> + <button + type="button" + class="button p-less" + data-style="outlined" + th:text="#{general.cancel}" + data-cancel></button> + <button + type="submit" + class="button p-less" + th:text="#{assignment.submit}"></button> + </div> + </form> + <div + th:id="|script-feedback-${testSubmission.id}|" + th:unless="${testSubmission == null || testScriptResults.isEmpty()}" + th:with="trainResult = ${testScriptResults[0]}"> + <div> + <th:block th:each="wagonResult, iter : ${trainResult.subresults}"> + <div class="accordion__header flex space-between pil-5 pbl-3"> + <div class="flex gap-3 align-center"> + <span + th:text="|${wagonResult.wagon.name} (#{|script.status.${wagonResult.status.name().toLowerCase()}|})|"></span> + <span + class="chip" + th:if="${wagonResult.status.name() == 'DONE' and wagonResult.score != null}" + th:text="${@gradeService.getScoreDisplayString(wagonResult.score, wagonResult.wagon.gradeScheme)}"></span> + </div> + <button + th:aria-controls="|testwagon-${wagonResult.wagon.id}|" + hidden></button> + </div> + + <div + class="flex gap-3 vertical mb-3" + th:id="|testwagon-${wagonResult.wagon.id}|"> + <div th:each="scriptResult : ${wagonResult.subresults}" class="pil-5"> + <div class="flex gap-3 align-center"> + <span + th:text="|${scriptResult.script.name} (#{|script.status.${scriptResult.status.name().toLowerCase()}|})|"></span> + <span + class="chip" + th:if="${scriptResult.status.name() == 'DONE' and scriptResult.score != null}" + th:text="${@gradeService.getScoreDisplayString(scriptResult.score, scriptResult.script.gradeScheme)}"></span> + </div> + + <div + th:if="${scriptResult.feedback.isEmpty()}" + class="font-200 mt-2"> + <p th:if="${scriptResult.status.isFinished()}">No feedback</p> + <p th:unless="${scriptResult.status.isFinished()}"> + No feedback yet + </p> + </div> + <th:block + th:each="feedback : ${scriptResult.feedback}" + th:if="${@authorizationService.canSubmitTestSubmission(assignment.id)}"> + <div class="font-200 mt-2"> + <p + style="max-width: 100ch" + th:each="text : ${#strings.listSplit(feedback.feedback, ' ')}" + th:text="${text}"></p> + </div> + </th:block> + </div> + </div> + </th:block> + </div> + </div> + </dialog> + </body> +</html> diff --git a/src/main/resources/templates/assignment/top.html b/src/main/resources/templates/assignment/top.html index faec249e4726efb2ea54004ce69941159094ad9e..80f4669ad1c575c886dcc99569372bb5bf4c6188 100644 --- a/src/main/resources/templates/assignment/top.html +++ b/src/main/resources/templates/assignment/top.html @@ -48,6 +48,9 @@ </div> <h1 class="font-800 mb-3" th:text="${assignment.name}"></h1> + <div th:if="${assignment.hidden}" class="banner mb-3" data-type="warning"> + <p th:text="#{assignment.hide.info}"></p> + </div> <div class="flex gap-3 wrap mb-5"> <button @@ -73,12 +76,29 @@ data-style="outlined" th:text="#{grading.import}" data-dialog="import-grades-overlay"></button> + <form + th:if="${@authorizationService.canEditAssignment(assignment.id)}" + th:id="toggle-hidden" + method="post" + th:action="@{|/assignment/${assignment.id}/change-visibility|}" + hidden></form> + <button + th:if="${@authorizationService.canEditAssignment(assignment.id)}" + class="button" + data-style="outlined" + th:form="toggle-hidden" + th:text="${assignment.hidden} ? #{assignment.show} : #{assignment.hide}"></button> <button th:if="${@authorizationService.canCreateVersion(assignment.id) and versions.isEmpty()}" class="button" data-style="outlined" th:text="#{script.train.add}" data-dialog="add-train-overlay"></button> + <button + th:if="${@authorizationService.canSubmitTestSubmission(assignment.id) and not versions.isEmpty()}" + th:data-dialog="|test-submission-${assignment.id}-overlay|" + class="button" + th:text="#{assignment.test_submission}"></button> </div> <div class="tabs mb-5" role="tablist"> @@ -108,6 +128,7 @@ <div th:replace="~{assignment/import_grades :: overlay}"></div> <div th:replace="~{assignment/edit :: overlay}"></div> <div th:replace="~{assignment/submit :: overlay('assignment')}"></div> + <div th:replace="~{assignment/test_submission :: overlay}"></div> <div th:replace="~{assignment/add_train :: overlay}"></div> </div> </body> diff --git a/src/main/resources/templates/edition/assist.html b/src/main/resources/templates/edition/assist.html index 9a38a6fca27cf74d22695787f1f95b882d0b1379..5f5a2d6502aabd71579f900364519a1f43762836 100644 --- a/src/main/resources/templates/edition/assist.html +++ b/src/main/resources/templates/edition/assist.html @@ -58,6 +58,7 @@ name="q" placeholder="Search for group name, netID, or student name..." th:value="${param.q}" /> + <input type="hidden" name="module" th:value="${module?.id}" /> <button type="submit" class="fa-solid fa-search"></button> </form> @@ -96,24 +97,26 @@ </div> </th:block> <script> + function fetchGroup(group) { + const toReplace = + group.nextElementSibling.querySelector("[data-fetch]"); + if (toReplace == null) return; + fetch(`/group/${toReplace.dataset.fetch}/submissions`) + .then(r => r.text()) + .then(elem => { + toReplace.parentElement.innerHTML = elem; + initComponentEvents( + document.getElementById( + `group-${toReplace.dataset.fetch}-submissions` + ) + ); + }) + .catch(error => { + console.error("Fetch error:", error); + toReplace.parentElement.innerHTML = `An error occurred when fetching the data`; + }); + } document.addEventListener("DOMContentLoaded", function () { - function fetchGroup(group) { - const toReplace = - group.nextElementSibling.querySelector("[data-fetch]"); - if (toReplace == null) return; - fetch(`/group/${toReplace.dataset.fetch}/submissions`) - .then(r => r.text()) - .then(elem => { - toReplace.parentElement.innerHTML = elem; - initComponentEvents( - document.getElementById( - `group-${toReplace.dataset.fetch}-submissions` - ) - ); - }) - .catch(() => window.location.reload()); - } - const groups = document.getElementById("groups"); groups.querySelectorAll(".accordion__header").forEach(g => g.addEventListener("click", function (event) { @@ -140,13 +143,10 @@ role="list" th:id="|group-${group.id}-submissions|"> <li - th:each="entry : ${group.submissionMap}" - th:with="assignment = ${entry.key}, submission = ${entry.value.isEmpty() ? null : entry.value[0]}" + th:each="key : ${@assignmentService.getVisibleAssignments(group.submissionMap.keySet())}" + th:with="assignment = ${key}, entryValue =${group.submissionMap.get(key)}, submission = ${entryValue.isEmpty() ? null : entryValue[0]}" class="pil-5 mb-3 flex space-between"> <div class="flex align-center gap-3"> - <span - th:unless="${@authorizationService.canViewAssignment(assignment.id)}" - th:text="${assignment.name}"></span> <a th:if="${@authorizationService.canViewAssignment(assignment.id)}" class="link" diff --git a/src/main/resources/templates/edition/student.html b/src/main/resources/templates/edition/student.html index 97dc36b7fd64be2732bf848f799e00d657844900..5777b3db82d9ecc946dd4d6f7d29a0c3e3f0fa3f 100644 --- a/src/main/resources/templates/edition/student.html +++ b/src/main/resources/templates/edition/student.html @@ -139,10 +139,12 @@ </ul> </div> - <div class="accordion" th:unless="${group == null}"> + <div + class="accordion" + th:unless="${group == null && !@assignmentService.getVisibleAssignments(submissionMap.entrySet()).isEmpty()}"> <th:block - th:each="entry : ${submissionMap}" - th:with="assignment = ${entry.key}, submissions = ${entry.value}"> + th:each="key : ${@assignmentService.getVisibleAssignments(submissionMap.keySet())}" + th:with="assignment = ${key}, submissions = ${submissionMap.get(assignment)}"> <div class="accordion__header"> <div class="flex space-between pil-5 pbl-3"> <div class="flex align-center gap-3"> diff --git a/src/main/resources/templates/edition/view.html b/src/main/resources/templates/edition/view.html index 44704367ef625a6de1985f6fdef0f45292aa5950..11eef9e75da5a6b1e1498bdf0b19245004729f91 100644 --- a/src/main/resources/templates/edition/view.html +++ b/src/main/resources/templates/edition/view.html @@ -73,15 +73,16 @@ </div> <ul th:id="|module-${module.id}|" + th:with="visibleAssignments = ${@assignmentService.getVisibleAssignments(module.assignments)}" class="accordion__content list flex vertical gap-3 mb-3" role="list" aria-expanded="false"> - <li th:if="${module.assignments.isEmpty()}" class="pil-5"> + <li th:if="${visibleAssignments.isEmpty()}" class="pil-5"> No assignments </li> <li class="pil-5" - th:each="assignment : ${#lists.sort(module.assignments, @templateService.nameComparator)}"> + th:each="assignment : ${#lists.sort(visibleAssignments, @templateService.nameComparator)}"> <a class="link" th:href="@{/assignment/{id}(id=${assignment.id})}" diff --git a/src/main/resources/templates/group/view.html b/src/main/resources/templates/group/view.html index 0fbda7bbe4f909cb14da897a60be5b949e21656c..d5e385bbbdb14cd2089c6a72b15a9af5877bc910 100644 --- a/src/main/resources/templates/group/view.html +++ b/src/main/resources/templates/group/view.html @@ -77,6 +77,10 @@ th:text="#{group.leave}"></button> </div> + <div class="banner mb-3" data-type="error" th:if="${error != null}"> + <p th:text="${error}"></p> + </div> + <div th:if="${@authorizationService.isStudentInModule(module.id)}" class="grid col-2 mb-5"> @@ -143,20 +147,15 @@ <ul id="assignments" class="accordion divided list" role="list"> <li class="pbl-3 pil-5" - th:each="entry : ${group.submissionMap.entrySet()}" - th:with="assignment = ${entry.key}, submissions = ${entry.value}, latest = ${submissions.isEmpty() ? null : submissions[0]}, grade = ${latest?.getMaxGrade()}"> + th:each="key : ${@assignmentService.getVisibleAssignments(group.submissionMap.keySet())}" + th:with="assignment = ${key}, submissions = ${group.submissionMap.get(key)}, latest = ${submissions.isEmpty() ? null : submissions[0]}, grade = ${latest?.getMaxGrade()}"> <div th:data-item="${assignment.id}" class="accordion__header"> <div class="flex space-between"> <div> <a - th:if="${@authorizationService.canViewAssignment(assignment.id)}" class="link" th:href="@{|/assignment/${assignment.id}|}" th:text="${assignment.name}"></a> - - <span - th:unless="${@authorizationService.canViewAssignment(assignment.id)}" - th:text="${assignment.name}"></span> <span class="chip" th:if="${grade != null}" diff --git a/src/main/resources/templates/module/create_groups.html b/src/main/resources/templates/module/create_groups.html index f559331db34d4d5f85c5c4915abaef5b1fdb0745..0cce7a6feeb25a438563dcd7df0fbec01dc834fa 100644 --- a/src/main/resources/templates/module/create_groups.html +++ b/src/main/resources/templates/module/create_groups.html @@ -34,6 +34,7 @@ <div class="flex gap-3"> <button class="button" data-dialog="allocate">Allocate groups</button> <button class="button" data-dialog="generate">Generate groups</button> + <button class="button" data-dialog="import">Import groups</button> </div> <dialog id="allocate" class="dialog"> @@ -103,6 +104,55 @@ </div> </form> </dialog> + + <dialog id="import" class="dialog"> + <form + class="flex vertical p-7" + th:action="@{|/group/${module.id}/import|}" + method="post" + enctype="multipart/form-data"> + <h2 class="underlined font-500" th:text="#{module.create_groups.import}"></h2> + <p> + Note: students must already be part of the edition. You can do so by using + the import feature. + </p> + <div class="grid col-2 align-center gap-3" style="--col-1: minmax(0, 8rem)"> + <label for="file" th:text="#{general.file}"></label> + <input + type="file" + id="file" + name="file" + accept="text/csv" + onchange="readColumnOptions()" + required /> + </div> + <h3>File format</h3> + <div class="surface"> + <div class="flex gap-2"> + <div class="grid align-center gap-0"> + <span class="font-100">1.</span> + <span class="font-100">2.</span> + <span class="font-100">3.</span> + </div> + <div class="grid gap-0"> + <code>Header 1,Header 2</code> + <code>NetID,group name</code> + <code>NetID,group name</code> + </div> + </div> + <em style="font-style: italic">The header row will be ignored</em> + </div> + <div class="flex space-between"> + <button class="button p-less" data-style="outlined" data-cancel> + Cancel + </button> + <button + class="button p-less" + type="submit" + th:text="#{module.create_groups.import}"></button> + </div> + </form> + </dialog> </main> </body> </html> diff --git a/src/main/resources/templates/module/groups.html b/src/main/resources/templates/module/groups.html index 1ad00a277db538c07e67f078388b9adacfedc711..2b8cdc8c17fdcdf0a7281e265b991d24bd42f956 100644 --- a/src/main/resources/templates/module/groups.html +++ b/src/main/resources/templates/module/groups.html @@ -43,6 +43,9 @@ <th:block layout:replace="~{module/top :: top}"></th:block> <div class="flex vertical"> + <div class="banner mb-3" data-type="error" th:if="${error != null}"> + <p th:text="${error}"></p> + </div> <form th:if="${@authorizationService.canViewModule(module.id)}" id="group-filter" diff --git a/src/main/resources/templates/module/view.html b/src/main/resources/templates/module/view.html index 8a36a0bb77a9235e7f2f064f5786d28b1cf58900..4ca37abb66b7ff993f91ccfa34c41fe86d3876c5 100644 --- a/src/main/resources/templates/module/view.html +++ b/src/main/resources/templates/module/view.html @@ -66,15 +66,11 @@ <ul id="assignments" class="divided list" role="list"> <li class="flex space-between pil-5 pbl-3" - th:each="assignment : ${#lists.sort(module.assignments, @templateService.nameComparator)}"> + th:each="assignment : ${#lists.sort(@assignmentService.getVisibleAssignments(module.assignments), @templateService.nameComparator)}"> <a - th:if="${@authorizationService.canViewAssignment(assignment.id)}" class="link" th:href="@{/assignment/{id}(id=${assignment.id})}" th:text="${assignment.name}"></a> - <span - th:unless="${@authorizationService.canViewAssignment(assignment.id)}" - th:text="${assignment.name}"></span> <a th:if="${@authorizationService.canViewAssignmentSubmissions(assignment.id)}" class="link" diff --git a/src/main/resources/templates/submission/feedback.html b/src/main/resources/templates/submission/feedback.html index c06b401c36fe41aa78843c2bf011c35243894287..3a8370f9032fb2ce82184904a30146c858c347db 100644 --- a/src/main/resources/templates/submission/feedback.html +++ b/src/main/resources/templates/submission/feedback.html @@ -193,11 +193,10 @@ <th:block th:each="feedback : ${scriptResult.feedback}" th:if="${@authorizationService.canViewGroupFeedback(group.id, feedback)}"> - <div class="font-200 mt-2"> + <div class="article mt-2"> <p style="max-width: 100ch" - th:each="text : ${#strings.listSplit(feedback.feedback, ' ')}" - th:text="${text}"></p> + th:utext="${@markdownService.markdownToHtml(feedback.feedback)}"></p> </div> </th:block> </div> diff --git a/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java b/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java index 9c689d7b1abc8200d21a08777bb08edbcc3f637f..8b5f59df73060d6fa809c3bc2d2cd778c20070e2 100644 --- a/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java +++ b/src/test/java/nl/tudelft/submit/controller/AssignmentControllerTest.java @@ -49,7 +49,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import application.test.TestSubmitApplication; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.api.GradeScheme; -import nl.tudelft.submit.cache.PersonCacheManager; import nl.tudelft.submit.dto.create.SubmitAssignmentCreateDTO; import nl.tudelft.submit.dto.create.note.AssignmentNoteCreateDTO; import nl.tudelft.submit.dto.helper.VersionDescription; @@ -62,8 +61,8 @@ import nl.tudelft.submit.dto.view.FeedbackViewDTO; import nl.tudelft.submit.dto.view.VersionViewDTO; import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO; import nl.tudelft.submit.dto.view.statistics.AssignmentStatisticsDTO; +import nl.tudelft.submit.model.TestSubmission; import nl.tudelft.submit.model.script.ScriptTrain; -import nl.tudelft.submit.repository.SubmitAssignmentRepository; import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.*; import nl.tudelft.submit.test.TestUserDetailsService; @@ -87,6 +86,7 @@ class AssignmentControllerTest { private static final SubmitAssignmentCreateDTO ASSIGNMENT_CREATE = new SubmitAssignmentCreateDTO( "Self Study", new Description(), null, null, GradeScheme.DUTCH_GRADE, null, null, null, null, null, + null, new ModuleIdDTO().id(MODULE_ID)); private static final SubmitAssignmentPatchDTO ASSIGNMENT_PATCH = new SubmitAssignmentPatchDTO(); private static final AssignmentModuleDetailsDTO ASSIGNMENT_DETAILS = new AssignmentModuleDetailsDTO() @@ -99,6 +99,7 @@ class AssignmentControllerTest { .submissions(new ArrayList<>()); private static final SubmitAssignmentDetailsDTO SUBMIT_ASSIGNMENT_DETAILS = SubmitAssignmentDetailsDTO .builder() + .id(ASSIGNMENT_ID) .name("Chapter 1") .module(new ModuleLayer1DTO() .id(MODULE_ID) @@ -157,12 +158,6 @@ class AssignmentControllerTest { @MockBean private ScriptService scriptService; - @Autowired - private SubmitAssignmentRepository assignmentRepository; - - @MockBean - private PersonCacheManager personCache; - @MockBean private EditionService editionService; @@ -181,6 +176,7 @@ class AssignmentControllerTest { verify(authService, never()).canEditAssignment(anyLong()); verify(assignmentService, never()).getAssignmentDetails(anyLong()); verify(versionService, never()).getVersionsPerAssignment(anyLong()); + verify(submissionService, never()).getLatestTestSubmissionByAssignment(anyLong()); verify(studentGroupService, never()).getGroupForPersonInModule(anyLong(), anyLong()); verify(versionService, never()).getVersionOfAssignmentForGroup(anyLong(), anyLong()); } @@ -195,6 +191,7 @@ class AssignmentControllerTest { when(assignmentService.isTransparent(anyLong())).thenReturn(false); when(authService.canEditAssignment(anyLong())).thenReturn(true); when(versionService.getVersionsPerAssignment(anyLong())).thenReturn(List.of(VERSION_VIEW)); + when(submissionService.getLatestTestSubmissionByAssignment(anyLong())).thenReturn(Optional.empty()); when(assignmentService.getLatestSubmissionsForAllGroups(anyLong())).thenReturn(List.of()); when(editionService.getEdition(anyLong())) @@ -209,9 +206,10 @@ class AssignmentControllerTest { .andExpect(model().attribute("assignment", SUBMIT_ASSIGNMENT_DETAILS)) .andExpect(model().attributeExists("versions")); - verify(authService).canViewAssignment(ASSIGNMENT_ID); + verify(authService, atLeastOnce()).canViewAssignment(ASSIGNMENT_ID); verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID); verify(versionService).getVersionsPerAssignment(ASSIGNMENT_ID); + verify(submissionService).getLatestTestSubmissionByAssignment(anyLong()); verify(studentGroupService, never()).getGroupForPersonInModule(anyLong(), anyLong()); verify(versionService, never()).getVersionOfAssignmentForGroup(anyLong(), anyLong()); } @@ -228,7 +226,7 @@ class AssignmentControllerTest { when(versionService.getVersionsPerAssignment(anyLong())).thenReturn(List.of(VERSION_VIEW)); when(scriptService.getScriptTrainById(anyLong())) .thenReturn(ScriptTrain.builder().wagons(Collections.emptyList()).build()); - + when(submissionService.getLatestTestSubmissionByAssignment(anyLong())).thenReturn(Optional.of(TestSubmission.builder().scriptResults(Collections.emptyList()).build())); when(assignmentService.getLatestSubmissionsForAllGroups(anyLong())).thenReturn(List.of()); when(editionService.getEdition(anyLong())) .thenReturn(new EditionDetailsDTO().course(new CourseSummaryDTO())); @@ -241,12 +239,15 @@ class AssignmentControllerTest { .andExpect(model().attributeExists("assignment")) .andExpect(model().attribute("assignment", SUBMIT_ASSIGNMENT_DETAILS)) .andExpect(model().attributeExists("train")) - .andExpect(model().attributeExists("versions")); + .andExpect(model().attributeExists("versions")) + .andExpect(model().attributeExists("testSubmission")) + .andExpect(model().attributeExists("testScriptResults")); - verify(authService).canViewAssignment(ASSIGNMENT_ID); + verify(authService, atLeastOnce()).canViewAssignment(ASSIGNMENT_ID); verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID); verify(versionService).getVersionsPerAssignment(ASSIGNMENT_ID); verify(scriptService).getScriptTrainById(SCRIPT_ID); + verify(submissionService).getLatestTestSubmissionByAssignment(anyLong()); verify(studentGroupService, never()).getGroupForPersonInModule(anyLong(), anyLong()); verify(versionService, never()).getVersionOfAssignmentForGroup(anyLong(), anyLong()); } @@ -278,7 +279,7 @@ class AssignmentControllerTest { .andExpect(model().attributeExists("versions", "submissions", "markSubmissionLate", "canMakeSubmission")); - verify(authService).canViewAssignment(ASSIGNMENT_ID); + verify(authService, atLeastOnce()).canViewAssignment(ASSIGNMENT_ID); verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID); verify(versionService).getVersionsPerAssignment(ASSIGNMENT_ID); @@ -353,7 +354,7 @@ class AssignmentControllerTest { mockMvc.perform(get("/assignment/{id}/statistics", ASSIGNMENT_ID)) .andExpect(status().isOk()); - verify(authService).canViewAssignmentStatistics(ASSIGNMENT_ID); + verify(authService, atLeastOnce()).canViewAssignmentStatistics(ASSIGNMENT_ID); verify(statisticsService).getAssignmentStatistics(any()); } @@ -497,11 +498,34 @@ class AssignmentControllerTest { .andExpect(status().isOk()) .andExpect(model().attributeExists("assignment", "submissions")); - verify(authService).canViewAssignmentSubmissions(ASSIGNMENT_ID); + verify(authService, atLeastOnce()).canViewAssignmentSubmissions(ASSIGNMENT_ID); verify(assignmentService).getSubmitAssignmentDetails(ASSIGNMENT_ID); verify(assignmentService).getAllSubmissionsFiltered(ASSIGNMENT_ID, "", false); } + @Test + @WithUserDetails("username") + void toggleHidePermitted() throws Exception { + when(authService.canEditAssignment(anyLong())).thenReturn(true); + doNothing().when(assignmentService).toggleHide(anyLong()); + mockMvc.perform(post("/assignment/{id}/change-visibility", ASSIGNMENT_ID).with(csrf())) + .andExpect(status().is3xxRedirection()); + + verify(authService).canEditAssignment(ASSIGNMENT_ID); + verify(assignmentService).toggleHide(ASSIGNMENT_ID); + } + + @Test + @WithUserDetails("username") + void toggleHideForbidden() throws Exception { + when(authService.canEditAssignment(anyLong())).thenReturn(false); + mockMvc.perform(post("/assignment/{id}/change-visibility", ASSIGNMENT_ID).with(csrf())) + .andExpect(status().isForbidden()); + + verify(authService).canEditAssignment(ASSIGNMENT_ID); + verify(assignmentService, never()).toggleHide(ASSIGNMENT_ID); + } + @Test @WithUserDetails("username") void importGradesVerifiesCanImportAssignmentGrades() throws Exception { diff --git a/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java b/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java index c01f94689034b5425fc533e950a8f1071d661f5c..bfdd3f00df07238c8e66b3626592fbd9a8e3fed7 100644 --- a/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java +++ b/src/test/java/nl/tudelft/submit/controller/EditionControllerTest.java @@ -49,7 +49,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import application.test.TestSubmitApplication; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.labracore.lib.api.GradeScheme; -import nl.tudelft.submit.csv.CSVService; import nl.tudelft.submit.dto.create.SubmitEditionCreateDTO; import nl.tudelft.submit.dto.create.grading.GradingFormulaCreateDTO; import nl.tudelft.submit.dto.helper.GradeExportOptions; @@ -111,8 +110,6 @@ public class EditionControllerTest { @MockBean private PersonService personService; @MockBean - private CSVService csvService; - @MockBean private ExportService exportService; @MockBean @@ -124,9 +121,6 @@ public class EditionControllerTest { @MockBean private ApiKeyService apiKeyService; - @MockBean - private FormulaService formulaService; - @MockBean private StatisticsService statisticsService; diff --git a/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java b/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java index 57a5c068eb3d3b642c25b12e0abc9551d0ae7836..d7be69f82d30597f82c0a2e1a2f3ef81d92bae7e 100644 --- a/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java +++ b/src/test/java/nl/tudelft/submit/controller/GradeControllerTest.java @@ -49,8 +49,8 @@ import nl.tudelft.submit.model.SubmitAssignment; import nl.tudelft.submit.model.grading.CalculatedScore; import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.AssignmentService; -import nl.tudelft.submit.service.FeedbackService; import nl.tudelft.submit.service.GradeService; +import nl.tudelft.submit.service.SubmissionService; @AutoConfigureMockMvc @SpringBootTest(classes = TestSubmitApplication.class) @@ -71,9 +71,9 @@ public class GradeControllerTest { @MockBean private GradeService gradeService; @MockBean - private FeedbackService feedbackService; - @MockBean private AssignmentService assignmentService; + @MockBean + private SubmissionService submissionService; @Test @WithUserDetails("username") @@ -97,6 +97,7 @@ public class GradeControllerTest { when(authService.canGradeSubmission(anyLong())).thenReturn(true); when(assignmentService.getOrCreateSubmitAssignment(anyLong())).thenReturn(SubmitAssignment.builder() .scoreType(GradeScheme.DUTCH_GRADE).build()); + when(submissionService.getSubmissionIdByCoreId(anyLong())).thenReturn(SUBMISSION_ID); mockMvc.perform(post("/grade/group/{groupId}/{assignmentId}", GROUP_ID, ASSIGNMENT_ID).with(csrf()) .flashAttr("feedbackCreate", @@ -104,6 +105,7 @@ public class GradeControllerTest { .grade(6.0).build())) .andExpect(status().is3xxRedirection()); + verify(submissionService).getSubmissionIdByCoreId(SUBMISSION_ID); verify(authService).canGradeSubmission(SUBMISSION_ID); verify(gradeService).addGradedFeedback(anyLong(), eq(ASSIGNMENT_ID), eq(GradedFeedbackCreateDTO.builder().submissionId(SUBMISSION_ID).textualFeedback("yeet") diff --git a/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java b/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java index a37347cbc3c66cfde8fd711e34ebf6c0b98b9c71..a2495fe70840c5f44ac96da79fa4d26581b4b3f2 100644 --- a/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java +++ b/src/test/java/nl/tudelft/submit/controller/NotificationControllerTest.java @@ -41,7 +41,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import application.test.TestSubmitApplication; import nl.tudelft.submit.dto.view.NotificationViewDTO; -import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.NotificationService; @AutoConfigureMockMvc @@ -56,9 +55,6 @@ public class NotificationControllerTest { @MockBean private NotificationService service; - @MockBean - private AuthorizationService authService; - private static final NotificationViewDTO NOTIFICATION_VIEW = new NotificationViewDTO("Test message", "/test", "username"); diff --git a/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java b/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java index d0fe4f3a75409ea0baf71a5025b8af426b73ea89..aa01a4cbedd68911c0e2eab415d2bef46205c534 100644 --- a/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java +++ b/src/test/java/nl/tudelft/submit/controller/StudentGroupControllerTest.java @@ -36,6 +36,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -47,6 +48,7 @@ import nl.tudelft.labracore.api.dto.*; import nl.tudelft.submit.dto.create.note.GroupNoteCreateDTO; import nl.tudelft.submit.dto.id.GroupId; import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO; +import nl.tudelft.submit.exception.GroupSwitchingException; import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.*; import nl.tudelft.submit.test.TestUserDetailsService; @@ -83,15 +85,6 @@ class StudentGroupControllerTest { @MockBean private AssignmentService assignmentService; - @MockBean - private SubmissionService submissionService; - - @MockBean - private FeedbackService feedbackService; - - @MockBean - private EmailService emailService; - @MockBean private EditionService editionService; @@ -225,6 +218,24 @@ class StudentGroupControllerTest { verify(groupService).changeCapacities(MODULE_ID, 2); } + @Test + @WithUserDetails("username") + void importStudentGroupsTest() throws Exception { + when(authService.canImportStudentGroups(anyLong())).thenReturn(true); + doNothing().when(groupService) + .importStudentGroups(anyLong(), any()); + MockMultipartFile file = new MockMultipartFile("file", "importfile".getBytes()); + mockMvc.perform( + multipart("/group/{moduleId}/import", MODULE_ID) + .file(file).with(csrf()) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/module/" + MODULE_ID + "/groups"));; + + verify(authService).canImportStudentGroups(MODULE_ID); + verify(groupService).importStudentGroups(MODULE_ID, file); + } + @Test @WithUserDetails("username") void generateGroupsAllowsIfCanCreateGroup() throws Exception { @@ -271,6 +282,60 @@ class StudentGroupControllerTest { verify(groupService).addPeopleToGroup(GROUP_ID, List.of("username")); } + @Test + @WithUserDetails("username") + void joinGroupDoesNotAllowIfAlreadyInAGroup() throws Exception { + when(authService.canJoinGroup(anyLong())).thenReturn(true); + doThrow(new GroupSwitchingException("Cannot add a person that already has a group")).when(groupService).addPeopleToGroup(any(),anyList()); + when(moduleService.getModuleById(anyLong())) + .thenReturn(new ModuleDetailsDTO().edition(new EditionSummaryDTO().id(1L)).id(MODULE_ID)); + + mockMvc.perform(post("/group/{id}/join", GROUP_ID).with(csrf())) + .andExpect(flash().attributeExists("error")) + .andExpect(redirectedUrl("/module/" + MODULE_ID + "/groups")); + + verify(authService).canJoinGroup(anyLong()); + verify(groupService).addPeopleToGroup(GROUP_ID, List.of("username")); + } + + @Test + @WithUserDetails("username") + void createGroupDoesNotAllowIfAlreadyInAGroup() throws Exception { + when(authService.canCreateGroup(any())).thenReturn(true); + doThrow(new GroupSwitchingException("Cannot create a group with a person that already has a group")).when(groupService).createGroup(any()); + when(moduleService.getModuleById(anyLong())) + .thenReturn(new ModuleDetailsDTO().edition(new EditionSummaryDTO().id(1L)).id(MODULE_ID)); + + StudentGroupCreateDTO create = + new StudentGroupCreateDTO().module(new ModuleIdDTO().id(MODULE_ID)) + .members(List.of(new RoleIdDTO().id(new Id().personId(1L)))); + + mockMvc.perform(post("/group", GROUP_ID).with(csrf()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .flashAttr("groupCreate", create)) + .andExpect(flash().attributeExists("error")) + .andExpect(redirectedUrl("/module/" + MODULE_ID + "/groups")); + + verify(groupService).createGroup(create); + } + + @Test + @WithUserDetails("username") + void addMembersDoesNotAllowIfAlreadyInGroup() throws Exception { + when(authService.canAddMembersToGroup(anyLong())).thenReturn(true); + doThrow(new GroupSwitchingException("Cannot add a person that already has a group")).when(groupService).addPeopleToGroup(any(),anyList()); + + mockMvc.perform( + post("/group/{id}/members", GROUP_ID).with(csrf()) + .param("usernames", "apond") + .param("usernames", "sholmes") + .param("usernames", "jsmith")) + .andExpect(flash().attributeExists("error")) + .andExpect(redirectedUrl("/group/" + GROUP_ID)); + + verify(groupService).addPeopleToGroup(GROUP_ID, List.of("apond", "sholmes", "jsmith")); + } + @Test @WithUserDetails("username") void addMembersToGroupVerifiesCanAddMembersToGroup() throws Exception { diff --git a/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java b/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java index ca0ebc9445d966826fe545c80bd19dd882440383..a83ddb4bf11eb16e53b52fee83b93d294f8bc315 100644 --- a/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java +++ b/src/test/java/nl/tudelft/submit/controller/SubmissionControllerTest.java @@ -58,7 +58,11 @@ import nl.tudelft.submit.dto.create.note.SubmissionNoteCreateDTO; import nl.tudelft.submit.dto.id.ScriptTrainIdDTO; import nl.tudelft.submit.dto.id.SubmissionId; import nl.tudelft.submit.dto.view.VersionViewDTO; +import nl.tudelft.submit.model.CoreSubmission; +import nl.tudelft.submit.model.TestSubmission; import nl.tudelft.submit.model.UserSettings; +import nl.tudelft.submit.model.Version; +import nl.tudelft.submit.model.script.ScriptTrain; import nl.tudelft.submit.security.AuthorizationService; import nl.tudelft.submit.service.*; @@ -95,6 +99,13 @@ class SubmissionControllerTest { .assignment(new AssignmentIdDTO().id(ASSIGNMENT_ID)) .group(new StudentGroupIdDTO().id(GROUP_ID)); + private static final CoreSubmission coreSubmission = CoreSubmission.builder() + .scriptPending(false) + .scriptResults(Collections.emptyList()) + .coreSubmissionId(SUBMISSION_ID) + .id(SUBMISSION_ID) + .build(); + @Value("${submit.filesys.storage-dir}") private String storageDir; @Value("${submit.filesys.temp-storage-dir}") @@ -142,12 +153,13 @@ class SubmissionControllerTest { when(submissionService.addSubmission(SUB_CREATE_DTO)).thenReturn(SUBMISSION_ID); when(submissionService.canMakeSubmission(anyLong(), anyLong())).thenReturn(true); when(submissionService.checkSubmission(anySet(), any())).thenReturn(true); + when(submissionService.getOrCreateSubmitCoreSubmission(anyLong())).thenReturn(coreSubmission); when(userSettingsService.getOrCreateUserSettings(any())).thenReturn(settings); when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO); when(groupService.getGroupForPersonInModule(anyLong(), anyLong())) .thenReturn(Optional.ofNullable(new StudentGroupDetailsDTO().id(GROUP_ID))); doNothing().when(fileService).uploadFiles(any(), anyLong(), anyString()); - doNothing().when(scriptService).runScriptTrain(anyLong(), anyLong()); + doNothing().when(scriptService).runScriptTrain(anyLong(), any()); mockMvc.perform(MockMvcRequestBuilders.multipart("/submission/group") .file(new MockMultipartFile("file", "somefile".getBytes())).with(csrf()) @@ -156,11 +168,65 @@ class SubmissionControllerTest { .andExpect(status().is3xxRedirection()); verify(fileService).uploadFiles(any(), anyLong(), anyString()); + verify(submissionService).getOrCreateSubmitCoreSubmission(anyLong()); verify(authService).canSubmitAssignment(anyLong(), anyLong()); verify(submissionService).addSubmission(SUB_CREATE_DTO); verify(emailService).sendEmailToAuthPerson(any(MailMessageCreateDTO.class)); } + @Test + @WithUserDetails("username") + void addTestSubmissionNoVersionId() throws Exception { + TestSubmission testSubmission = TestSubmission.builder().id(3L).assignmentId(ASSIGNMENT_ID).build(); + when(authService.canSubmitTestSubmission(anyLong())).thenReturn(true); + when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO); + when(submissionService.checkSubmission(anySet(), any())).thenReturn(true); + when(submissionService.createTestSubmissionForAssignment(anyLong())).thenReturn(testSubmission); + when(versionService.getVersionsPerAssignment(anyLong())) + .thenReturn(List.of(VersionViewDTO.builder().scriptTrain(new ScriptTrainIdDTO(4L)).build())); + doNothing().when(fileService).uploadFiles(any(), anyLong(), anyString()); + doNothing().when(scriptService).runScriptTrain(anyLong(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.multipart("/submission/test-submission/{assignmentId}", ASSIGNMENT_ID) + .file(new MockMultipartFile("file", "somefile".getBytes())).with(csrf()) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().is3xxRedirection()); + + verify(fileService).uploadFiles(any(), anyLong(), anyString()); + verify(authService).canSubmitTestSubmission(anyLong()); + verify(submissionService).checkSubmission(any(), any()); + verify(submissionService).createTestSubmissionForAssignment(ASSIGNMENT_ID); + } + + @Test + @WithUserDetails("username") + void addTestSubmissionWithVersionId() throws Exception { + TestSubmission testSubmission = TestSubmission.builder().id(3L).assignmentId(ASSIGNMENT_ID).build(); + when(authService.canSubmitTestSubmission(anyLong())).thenReturn(true); + when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO); + when(submissionService.checkSubmission(anySet(), any())).thenReturn(true); + when(submissionService.createTestSubmissionForAssignment(anyLong())).thenReturn(testSubmission); + when(versionService.getVersion(anyLong())) + .thenReturn(Version.builder().scriptTrain(ScriptTrain.builder().id(4L).build()).build()); + doNothing().when(fileService).uploadFiles(any(), anyLong(), anyString()); + doNothing().when(scriptService).runScriptTrain(anyLong(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.multipart("/submission/test-submission/{assignmentId}", ASSIGNMENT_ID) + .file(new MockMultipartFile("file", "somefile".getBytes())) + .param("versionId", String.valueOf(6L)).with(csrf()) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().is3xxRedirection()); + + verify(fileService).uploadFiles(any(), anyLong(), anyString()); + verify(authService).canSubmitTestSubmission(anyLong()); + verify(submissionService).checkSubmission(any(), any()); + verify(versionService, never()).getVersionsPerAssignment(anyLong()); + verify(versionService).getVersion(anyLong()); + verify(submissionService).createTestSubmissionForAssignment(ASSIGNMENT_ID); + } + @Test @WithUserDetails("username") void addSubmissionDoesNotSendOnError() throws Exception { @@ -168,13 +234,14 @@ class SubmissionControllerTest { when(authService.canSubmitAssignment(anyLong(), anyLong())).thenReturn(true); when(submissionService.addSubmission(SUB_CREATE_DTO)).thenReturn(SUBMISSION_ID); + when(submissionService.getOrCreateSubmitCoreSubmission(anyLong())).thenReturn(coreSubmission); when(submissionService.canMakeSubmission(anyLong(), anyLong())).thenReturn(true); when(submissionService.checkSubmission(anySet(), any())).thenReturn(true); when(assignmentService.getAssignmentDetails(anyLong())).thenReturn(ASSIGNMENT_DETAILS_DTO); when(groupService.getGroupForPersonInModule(anyLong(), anyLong())) .thenReturn(Optional.ofNullable(new StudentGroupDetailsDTO().id(GROUP_ID))); doThrow(new IOException()).when(fileService).uploadFiles(any(), anyLong(), anyString()); - doNothing().when(scriptService).runScriptTrain(anyLong(), anyLong()); + doNothing().when(scriptService).runScriptTrain(anyLong(), any()); mockMvc.perform(MockMvcRequestBuilders.multipart("/submission/group") .file(new MockMultipartFile("file", "somefile".getBytes())).with(csrf()) @@ -183,6 +250,7 @@ class SubmissionControllerTest { .andExpect(status().is3xxRedirection()); verify(fileService).uploadFiles(any(), anyLong(), anyString()); + verify(submissionService).getOrCreateSubmitCoreSubmission(anyLong()); verify(authService).canSubmitAssignment(anyLong(), anyLong()); verify(submissionService).addSubmission(SUB_CREATE_DTO); verify(emailService, never()).sendEmailToAuthPerson(any(MailMessageCreateDTO.class)); @@ -214,7 +282,7 @@ class SubmissionControllerTest { when(groupService.getGroupForPersonInModule(anyLong(), anyLong())) .thenReturn(Optional.ofNullable(new StudentGroupDetailsDTO().id(GROUP_ID))); doThrow(new IOException()).when(fileService).uploadFiles(any(), anyLong(), anyString()); - doNothing().when(scriptService).runScriptTrain(anyLong(), anyLong()); + doNothing().when(scriptService).runScriptTrain(anyLong(), any()); mockMvc.perform(MockMvcRequestBuilders.multipart("/submission/group") .file(new MockMultipartFile("file.yeet", "somefile".getBytes())).with(csrf()) @@ -267,6 +335,7 @@ class SubmissionControllerTest { @WithUserDetails("username") void downloadSubmissionAllowsIfCanDownloadSubmission() throws Exception { when(authService.canDownloadSubmission(anyLong())).thenReturn(true); + when(submissionService.getSubmissionIdByCoreId(anyLong())).thenReturn(SUBMISSION_ID); String submissionPath = "./testfiles"; File dir = new File(submissionPath); @@ -283,6 +352,7 @@ class SubmissionControllerTest { .andExpect(status().isOk()); verify(authService).canDownloadSubmission(SUBMISSION_ID); + verify(submissionService).getSubmissionIdByCoreId(anyLong()); verify(fileService).downloadFiles(SUBMISSION_ID, storageDir + "/submissions", tempStorageDir + "/downloads"); @@ -298,6 +368,7 @@ class SubmissionControllerTest { mockMvc.perform(get("/submission/{submissionId}/download", SUBMISSION_ID)) .andExpect(status().isForbidden()); + verify(submissionService, never()).getSubmissionIdByCoreId(anyLong()); verify(authService).canDownloadSubmission(SUBMISSION_ID); verifyNoInteractions(fileService); } @@ -475,7 +546,7 @@ class SubmissionControllerTest { .andExpect(status().isForbidden()); verify(authService).canResubmitSubmission(SUBMISSION_ID); - verify(scriptService, never()).runScriptTrain(anyLong(), anyLong()); + verify(scriptService, never()).runScriptTrain(anyLong(), any()); } @Test @@ -494,7 +565,7 @@ class SubmissionControllerTest { .andExpect(status().isOk()); verify(authService).canResubmitSubmission(SUBMISSION_ID); - verify(scriptService).runScriptTrain(anyLong(), anyLong()); + verify(scriptService).runScriptTrain(anyLong(), any()); } @ParameterizedTest diff --git a/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java b/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java index 1396444937f0dc7d982cbbf61c7f478fedc1bcb2..64fa0d582b0d683e2d480c44a05ade40d35577ff 100644 --- a/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java +++ b/src/test/java/nl/tudelft/submit/security/AuthorizationServiceTest.java @@ -45,10 +45,12 @@ import nl.tudelft.submit.cache.*; import nl.tudelft.submit.dto.id.ScriptTrainIdDTO; import nl.tudelft.submit.dto.view.FeedbackViewDTO; import nl.tudelft.submit.dto.view.VersionViewDTO; +import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO; import nl.tudelft.submit.model.Version; import nl.tudelft.submit.model.script.SubmissionKey; import nl.tudelft.submit.repository.SubmissionKeyRepository; import nl.tudelft.submit.repository.VersionRepository; +import nl.tudelft.submit.service.AssignmentService; import nl.tudelft.submit.service.VersionService; import nl.tudelft.submit.test.TestUserDetailsService; import reactor.core.publisher.Mono; @@ -92,6 +94,8 @@ class AuthorizationServiceTest { @MockBean private AssignmentCacheManager assignmentCache; @MockBean + private AssignmentService assignmentService; + @MockBean private SubmissionCacheManager submissionCache; @MockBean private StudentGroupCacheManager groupCache; @@ -640,8 +644,22 @@ class AuthorizationServiceTest { @ParameterizedTest @CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,true", "STUDENT,true", ",false" }) @WithUserDetails("username") - void canViewAssignment(RoleDetailsDTO.TypeEnum role, boolean expected) { + void canViewPublishedAssignment(RoleDetailsDTO.TypeEnum role, boolean expected) { + mockRole(role); + SubmitAssignmentDetailsDTO assignment = SubmitAssignmentDetailsDTO.builder().id(ASSIGNMENT_ID) + .hidden(false).build(); + when(assignmentService.getSubmitAssignmentDetails(anyLong())).thenReturn(assignment); + assertThat(service.canViewAssignment(ASSIGNMENT_ID)).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,false", "STUDENT,false", ",false" }) + @WithUserDetails("username") + void canViewHiddenAssignment(RoleDetailsDTO.TypeEnum role, boolean expected) { mockRole(role); + SubmitAssignmentDetailsDTO assignment = SubmitAssignmentDetailsDTO.builder().id(ASSIGNMENT_ID) + .hidden(true).build(); + when(assignmentService.getSubmitAssignmentDetails(anyLong())).thenReturn(assignment); assertThat(service.canViewAssignment(ASSIGNMENT_ID)).isEqualTo(expected); } @@ -856,6 +874,14 @@ class AuthorizationServiceTest { assertThat(service.canCreateGroup(MODULE_ID)).isEqualTo(expected); } + @ParameterizedTest + @CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,false", "STUDENT,false", ",false" }) + @WithUserDetails("username") + void canImportStudentGroups(RoleDetailsDTO.TypeEnum role, boolean expected) { + mockRole(role); + assertThat(service.canImportStudentGroups(MODULE_ID)).isEqualTo(expected); + } + @ParameterizedTest @CsvSource({ "TEACHER,true", "HEAD_TA,true", "TA,true", "STUDENT,false", ",false" }) @WithUserDetails("username") diff --git a/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java b/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java index 21f8cfb6aae6c5546a6f24df0a076eed6c3e02a4..3e92a8df2a7872b0747b91707f2a520ab612e9e5 100644 --- a/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java +++ b/src/test/java/nl/tudelft/submit/service/AssignmentServiceTest.java @@ -21,8 +21,7 @@ import static java.time.LocalDateTime.now; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import java.io.File; @@ -31,6 +30,7 @@ import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Paths; import java.time.LocalDateTime; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -59,12 +59,14 @@ import nl.tudelft.submit.dto.patch.SubmitAssignmentPatchDTO; import nl.tudelft.submit.dto.patch.grading.AssignmentGradeImportDTO; import nl.tudelft.submit.dto.view.VersionViewDTO; import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentDetailsDTO; +import nl.tudelft.submit.dto.view.labracore.SubmitAssignmentSummaryDTO; import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO; import nl.tudelft.submit.dto.view.labracore.SubmitSubmissionViewDTO; import nl.tudelft.submit.enums.GradeImportIdType; +import nl.tudelft.submit.model.CoreSubmission; import nl.tudelft.submit.model.SubmitAssignment; -import nl.tudelft.submit.model.SubmitSubmission; import nl.tudelft.submit.repository.SubmitAssignmentRepository; +import nl.tudelft.submit.security.AuthorizationService; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -102,6 +104,9 @@ class AssignmentServiceTest { @Autowired private SubmitAssignmentRepository assignmentRepository; + @MockBean + private AuthorizationService authorizationService; + @Autowired private AssignmentService assignmentService; @@ -317,8 +322,8 @@ class AssignmentServiceTest { .id(SUBMISSION_ID + 2) .group(new StudentGroupSummaryDTO().id(GROUP_ID + 1)) .submissionTime(now()))); - when(submissionService.getOrCreateSubmitSubmission(anyLong())) - .thenReturn(SubmitSubmission.builder().build()); + when(submissionService.getOrCreateSubmitCoreSubmission(anyLong())) + .thenReturn(CoreSubmission.builder().build()); List<SubmitSubmissionViewDTO> result = assignmentService.getAllSubmissions(ASSIGNMENT_ID); @@ -405,6 +410,61 @@ class AssignmentServiceTest { Files.delete(Paths.get(assignmentPath)); } + @Test + public void testToggleHide_shouldHideAssignment() { + SubmitAssignment assignment = assignmentService.getOrCreateSubmitAssignment(ASSIGNMENT_ID); + assignmentService.toggleHide(ASSIGNMENT_ID); + assertFalse(assignment.getHidden()); + assertFalse(assignmentService.getOrCreateSubmitAssignment(ASSIGNMENT_ID).getHidden()); + } + + @Test + void testGetVisibleAssignmentsAllAreVisible() { + List<SubmitAssignmentSummaryDTO> allAssignments = Arrays.asList( + SubmitAssignmentSummaryDTO.builder().id(1L).build(), + SubmitAssignmentSummaryDTO.builder().id(2L).build(), + SubmitAssignmentSummaryDTO.builder().id(3L).build()); + when(authorizationService.canViewAssignment(anyLong())).thenReturn(true); + + List<SubmitAssignmentSummaryDTO> visibleAssignments = assignmentService + .getVisibleAssignments(allAssignments); + + assertEquals(3, visibleAssignments.size()); + assertEquals(visibleAssignments, allAssignments); + } + + @Test + void testGetVisibleAssignmentsNoneAreVisible() { + List<SubmitAssignmentSummaryDTO> allAssignments = Arrays.asList( + SubmitAssignmentSummaryDTO.builder().id(1L).build(), + SubmitAssignmentSummaryDTO.builder().id(2L).build(), + SubmitAssignmentSummaryDTO.builder().id(3L).build()); + when(authorizationService.canViewAssignment(anyLong())).thenReturn(false); + + List<SubmitAssignmentSummaryDTO> visibleAssignments = assignmentService + .getVisibleAssignments(allAssignments); + + assertTrue(visibleAssignments.isEmpty()); + } + + @Test + void testGetVisibleAssignmentsSomeAreVisible() { + SubmitAssignmentSummaryDTO visibleAssignment = SubmitAssignmentSummaryDTO.builder().id(2L).build(); + List<SubmitAssignmentSummaryDTO> allAssignments = Arrays.asList( + SubmitAssignmentSummaryDTO.builder().id(1L).build(), + visibleAssignment, + SubmitAssignmentSummaryDTO.builder().id(3L).build()); + when(authorizationService.canViewAssignment(1L)).thenReturn(false); + when(authorizationService.canViewAssignment(2L)).thenReturn(true); + when(authorizationService.canViewAssignment(3L)).thenReturn(false); + + List<SubmitAssignmentSummaryDTO> visibleAssignments = assignmentService + .getVisibleAssignments(allAssignments); + + assertEquals(1, visibleAssignments.size()); + assertEquals(visibleAssignments.get(0), visibleAssignment); + } + @Test void createSubmitAssignment() { assertThat(assignmentRepository.findById(ASSIGNMENT_ID)).isNotPresent(); @@ -421,6 +481,7 @@ class AssignmentServiceTest { .id(ASSIGNMENT_ID) .module(new ModuleLayer1DTO().id(MODULE_ID)) .scoreType(GradeScheme.DUTCH_GRADE) + .hidden(true) .allowedFileTypes(emptyList()) .build(); assertThat(assignmentService.getSubmitAssignmentDetails(ASSIGNMENT_ID)).isEqualTo(expected); diff --git a/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java b/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java index b354b69d675822934fa830048327b067e75bfe6c..bfae1318eba8111632ce4f5500efe1901eda39a9 100644 --- a/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java +++ b/src/test/java/nl/tudelft/submit/service/StudentGroupServiceTest.java @@ -18,10 +18,13 @@ package nl.tudelft.submit.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.io.IOException; +import java.io.InputStreamReader; import java.time.LocalDateTime; import java.util.*; @@ -32,12 +35,17 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; + +import com.opencsv.exceptions.CsvValidationException; import application.test.TestSubmitApplication; import nl.tudelft.labracore.api.StudentGroupControllerApi; import nl.tudelft.labracore.api.dto.*; import nl.tudelft.librador.dto.view.View; +import nl.tudelft.submit.cache.PersonCacheManager; import nl.tudelft.submit.cache.StudentGroupCacheManager; +import nl.tudelft.submit.csv.CSVService; import nl.tudelft.submit.dto.id.GroupId; import nl.tudelft.submit.dto.view.labracore.SubmitGroupDetailsDTO; import nl.tudelft.submit.dto.view.labracore.SubmitGroupSummaryDTO; @@ -45,6 +53,7 @@ import nl.tudelft.submit.enums.SwitchAction; import nl.tudelft.submit.exception.GroupSwitchingException; import nl.tudelft.submit.model.*; import nl.tudelft.submit.model.note.GroupNote; +import nl.tudelft.submit.repository.SubmitGroupRepository; import nl.tudelft.submit.repository.SwitchEventRepository; import nl.tudelft.submit.repository.note.GroupNoteRepository; import reactor.core.publisher.Flux; @@ -56,6 +65,7 @@ class StudentGroupServiceTest { private final Long GROUP_ID = 5L; private final Long MODULE_ID = 42L; + private final Long EDITION_ID = 33L; private final Long COURSE_ID = 139L; private final Long MEMBER_ID = 56123L; private final Long PERSON_ID = 98451L; @@ -65,6 +75,9 @@ class StudentGroupServiceTest { @MockBean private StudentGroupCacheManager groupCache; + @MockBean + private PersonCacheManager personCache; + @Autowired private StudentGroupControllerApi groupApi; @@ -80,6 +93,9 @@ class StudentGroupServiceTest { @Autowired private GroupNoteRepository groupNoteRepository; + @MockBean + private SubmitGroupRepository groupRepository; + @MockBean private ModuleDivisionService moduleDivisionService; @@ -89,6 +105,12 @@ class StudentGroupServiceTest { @MockBean private SubmissionService submissionService; + @MockBean + private CSVService csvService; + + @MockBean + private ModuleService moduleService; + @Test void getGroupDetails() { StudentGroupDetailsDTO dto = new StudentGroupDetailsDTO().id(GROUP_ID).name("Test group") @@ -203,6 +225,10 @@ class StudentGroupServiceTest { List<String> usernames = List.of("hpotter, rweasley, hgranger"); Mockito.when(groupApi.isGroupLocked(anyLong())).thenReturn(Mono.just(false)); Mockito.when(groupApi.addGroupMembers(anyLong(), anyList())).thenReturn(Mono.empty()); + Mockito.when(groupCache.getOrThrow(any())).thenReturn( + new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID))); + Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L)); + Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty()); groupService.addPeopleToGroup(GROUP_ID, usernames); @@ -210,9 +236,49 @@ class StudentGroupServiceTest { Mockito.verify(groupApi).addGroupMembers(GROUP_ID, usernames); } + @Test + public void importStudentGroupsTest() throws IOException, CsvValidationException { + MockMultipartFile file = new MockMultipartFile("file", "test.csv", "text/csv", "content".getBytes()); + List<String[]> lines = List.of( + new String[] { "dduck", "Group1" }, + new String[] { "mmouse", "Group1" }, + new String[] { "batman", "Group2" }); + PersonSummaryDTO person1 = new PersonSummaryDTO().id(1L).username("dduck").displayName("Donald Duck"); + PersonSummaryDTO person2 = new PersonSummaryDTO().id(2L).username("mmouse") + .displayName("Mickey Mouse"); + PersonSummaryDTO person3 = new PersonSummaryDTO().id(3L).username("batman").displayName("Batman"); + + when(csvService.readCSV(any(InputStreamReader.class), eq(false), eq(1))).thenReturn(lines); + when(moduleService.getModuleById(eq(MODULE_ID))).thenReturn( + new ModuleDetailsDTO().id(MODULE_ID).edition(new EditionSummaryDTO().id(EDITION_ID))); + when(personCache.get(eq("dduck"))).thenReturn(person1); + when(personCache.get(eq("mmouse"))).thenReturn(person2); + when(personCache.get(eq("batman"))).thenReturn(person3); + when(personCache.get(eq(1L))).thenReturn(Optional.ofNullable(person1)); + when(personCache.get(eq(2L))).thenReturn(Optional.ofNullable(person2)); + when(personCache.get(eq(3L))).thenReturn(Optional.ofNullable(person3)); + when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty()); + when(groupApi.addGroup(any(StudentGroupCreateDTO.class))).thenReturn(Mono.just(5L)); + when(groupRepository.save(any(SubmitGroup.class))).thenReturn(SubmitGroup.builder().id(5L).build()); + + groupService.importStudentGroups(MODULE_ID, file); + + verify(csvService).readCSV(any(InputStreamReader.class), eq(false), eq(1)); + verify(moduleService, times(3)).getModuleById(eq(MODULE_ID)); + verify(personCache, times(6)).get(anyString()); + verify(personCache, times(3)).get(anyLong()); + verify(groupRepository, times(2)).save(any(SubmitGroup.class)); + } + @Test void addPeopleToLockedGroup() { List<String> usernames = List.of("hpotter", "rweasley", "hgranger"); + + Mockito.when(groupCache.getOrThrow(any())).thenReturn( + new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID))); + Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L)); + Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty()); + Mockito.when(groupApi.isGroupLocked(anyLong())).thenReturn(Mono.just(true)); Mockito.when(groupApi.addGroupMembers(anyLong(), anyList())).thenReturn(Mono.empty()); @@ -227,16 +293,77 @@ class StudentGroupServiceTest { Mockito.when(groupApi.isGroupLockedAndRoleStudent(anyLong(), anyLong())).thenReturn(Mono.just(false)); Mockito.when(groupApi.addMemberToGroup(anyLong(), anyLong())).thenReturn(Mono.empty()); + Mockito.when(groupCache.getOrThrow(any())).thenReturn( + new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID))); + Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L)); + Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty()); + assertThat(groupService.addPersonToGroup(MEMBER_ID, GROUP_ID)).isEqualTo(-1L); Mockito.verify(groupApi).addMemberToGroup(GROUP_ID, MEMBER_ID); Mockito.verify(groupApi).isGroupLockedAndRoleStudent(MEMBER_ID, GROUP_ID); } + @Test + void cantAddStudentWhenStudentIsAlreadyInAGroup() { + StudentGroupDetailsDTO groupDetails = new StudentGroupDetailsDTO().id(77L) + .module(new ModuleSummaryDTO().id(MODULE_ID)).name("group name"); + Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())) + .thenReturn(Mono.just(groupDetails)); + Mockito.when(groupCache.getOrThrow(anyLong())).thenReturn(groupDetails); + Mockito.when(personCache.get(anyLong())) + .thenReturn(Optional.of(new PersonSummaryDTO().displayName("person name"))); + + assertThatThrownBy(() -> groupService.addPersonToGroup(MEMBER_ID, GROUP_ID)) + .isInstanceOf(GroupSwitchingException.class) + .hasMessage("Cannot add person to group because person name is already in group name"); + + } + + @Test + void cantCreateGroupWhenStudentIsAlreadyInAGroup() { + StudentGroupCreateDTO create = new StudentGroupCreateDTO().module(new ModuleIdDTO().id(MODULE_ID)) + .members(List.of(new RoleIdDTO().id(new Id().personId(MEMBER_ID)))); + Mockito.when(personCache.get(MEMBER_ID)).thenReturn(Optional + .of(new PersonSummaryDTO().username("personname").displayName("person name").id(MEMBER_ID))); + Mockito.when(personCache.get("personname")).thenReturn(new PersonSummaryDTO().id(MEMBER_ID)); + StudentGroupDetailsDTO groupDetails = new StudentGroupDetailsDTO().id(77L).name("group name"); + Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())) + .thenReturn(Mono.just(groupDetails)); + Mockito.when(groupCache.getOrThrow(anyLong())).thenReturn(groupDetails); + + assertThatThrownBy(() -> groupService.createGroup(create)) + .isInstanceOf(GroupSwitchingException.class) + .hasMessage("Group cannot be created because person name is already in group name"); + + } + + @Test + void cantAddStudentsWhenOneIsAlreadyInAGroup() { + List<String> usernames = List.of("hpotter", "rweasley", "hgranger"); + StudentGroupDetailsDTO groupDetails = new StudentGroupDetailsDTO().id(77L).name("group name"); + Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())) + .thenReturn(Mono.just(groupDetails)); + Mockito.when(groupCache.getOrThrow(any())).thenReturn( + new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID))); + Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(MEMBER_ID)); + Mockito.when(personCache.get(MEMBER_ID)).thenReturn(Optional + .of(new PersonSummaryDTO().id(MEMBER_ID).username("personname").displayName("person name"))); + + assertThatThrownBy(() -> groupService.addPeopleToGroup(MEMBER_ID, usernames)) + .isInstanceOf(GroupSwitchingException.class) + .hasMessage("Cannot add person to group because person name is already in group name"); + + } + @Test void addStudentToLockedGroup() { Mockito.when(groupApi.isGroupLockedAndRoleStudent(anyLong(), anyLong())).thenReturn(Mono.just(true)); Mockito.when(groupApi.addMemberToGroup(anyLong(), anyLong())).thenReturn(Mono.empty()); + Mockito.when(groupCache.getOrThrow(any())).thenReturn( + new StudentGroupDetailsDTO().id(GROUP_ID).module(new ModuleSummaryDTO().id(MODULE_ID))); + Mockito.when(personCache.get(anyString())).thenReturn(new PersonSummaryDTO().id(22L)); + Mockito.when(groupApi.getGroupForPersonAndModule(anyLong(), anyLong())).thenReturn(Mono.empty()); Long eventId = groupService.addPersonToGroup(MEMBER_ID, GROUP_ID); SwitchEvent event = switchEventRepository.findByIdOrThrow(eventId); diff --git a/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java b/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java index e5f1bdf0b3656806f59489e9fca114d7481bde3f..d46192201357ccbab5e4d26cf07606d54df8ba39 100644 --- a/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java +++ b/src/test/java/nl/tudelft/submit/service/SubmissionServiceTest.java @@ -18,6 +18,7 @@ package nl.tudelft.submit.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; import java.io.File; @@ -43,7 +44,12 @@ import nl.tudelft.labracore.api.dto.*; import nl.tudelft.submit.cache.SubmissionCacheManager; import nl.tudelft.submit.dto.create.SubmissionDownloadConfigCreateDTO; import nl.tudelft.submit.enums.SubmissionDownloadPreference; -import nl.tudelft.submit.repository.SubmitSubmissionRepository; +import nl.tudelft.submit.model.AbstractSubmitSubmission; +import nl.tudelft.submit.model.CoreSubmission; +import nl.tudelft.submit.model.TestSubmission; +import nl.tudelft.submit.repository.AbstractSubmitSubmissionRepository; +import nl.tudelft.submit.repository.CoreSubmissionRepository; +import nl.tudelft.submit.repository.TestSubmissionRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -79,17 +85,16 @@ class SubmissionServiceTest { @Autowired private SubmissionService submissionService; - @Autowired - private SubmitSubmissionRepository submissionRepository; - @MockBean private AssignmentService assignmentService; + @MockBean + private CoreSubmissionRepository submissionRepository; @MockBean - private StudentGroupService groupService; + private TestSubmissionRepository testSubmissionRepository; @MockBean - private GradeService gradeService; + private AbstractSubmitSubmissionRepository<AbstractSubmitSubmission> abstractSubmitSubmissionRepository; @MockBean private FileService fileService; @@ -419,12 +424,54 @@ class SubmissionServiceTest { .isEqualTo(List.of(Objects.requireNonNull(SUB_DETAILED_DTO.getId()))); } + @Test + void getSubmitSubmissionByIdTest() { + when(abstractSubmitSubmissionRepository.findById(SUBMISSION_ID)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build())); + assertEquals(submissionService.getSubmitSubmissionById(SUBMISSION_ID), CoreSubmission.builder().id(SUBMISSION_ID).build()); + } + + @Test + void getSubmissionIdByCoreIdTest() { + when(submissionRepository.findByCoreSubmissionId(SUBMISSION_ID)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build())); + assertEquals(submissionService.getSubmissionIdByCoreId(SUBMISSION_ID), SUBMISSION_ID); + } + + @Test + void getCoreSubBySubmitSubIdTest() { + CoreSubmission coreSubmission = CoreSubmission.builder().id(SUBMISSION_ID).build(); + when(submissionRepository.findById(SUBMISSION_ID)).thenReturn(Optional.of(coreSubmission)); + assertEquals(submissionService.getCoreSubBySubmitSubId(SUBMISSION_ID), Optional.of(coreSubmission)); + } + + @Test + void createTestSubmissionForAssignmentTest() { + TestSubmission testSubmission = TestSubmission.builder().id(SUBMISSION_ID).assignmentId(ASSIGNMENT_ID) + .build(); + when(testSubmissionRepository.save(any())).thenReturn(testSubmission); + TestSubmission submission = submissionService.createTestSubmissionForAssignment(ASSIGNMENT_ID); + verify(testSubmissionRepository).save(argThat(sub -> sub.getAssignmentId().equals(ASSIGNMENT_ID) && + sub.getCreatedDate() != null)); + assertEquals(testSubmission, submission); + } + + @Test + void getLatestTestSubmissionByAssignmentTest() { + TestSubmission testSubmission = TestSubmission.builder().id(1L).assignmentId(ASSIGNMENT_ID).build(); + TestSubmission testSubmission2 = TestSubmission.builder().id(2L).assignmentId(ASSIGNMENT_ID).build(); + when(testSubmissionRepository.findAllByAssignmentId(anyLong())) + .thenReturn(List.of(testSubmission, testSubmission2)); + assertEquals(submissionService.getLatestTestSubmissionByAssignment(ASSIGNMENT_ID), + Optional.of(testSubmission2)); + } + @Test void downloadOneSubmission() throws IOException { + when(submissionRepository.findByCoreSubmissionId(anyLong())).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build())); submissionService.downloadSubmissions(List.of(SUBMISSION_ID)); verify(fileService).downloadFiles(SUBMISSION_ID, storageDir + File.separator + "submissions", tempStorageDir + File.separator + "downloads"); + verify(submissionRepository).findByCoreSubmissionId(SUBMISSION_ID); } @Test @@ -440,6 +487,8 @@ class SubmissionServiceTest { doCallRealMethod().when(fileService).deleteDirectory(any()); when(fileService.getDirectory(anyLong())).thenReturn("s1").thenReturn("s2"); when(fileService.toFileName(anyString())).thenCallRealMethod(); + when(submissionRepository.findByCoreSubmissionId(SUBMISSION_ID)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID).build())); + when(submissionRepository.findByCoreSubmissionId(SUBMISSION_ID+1)).thenReturn(Optional.of(CoreSubmission.builder().id(SUBMISSION_ID+1).build())); File dir1 = new File(storageDir + File.separator + "submissions" + File.separator + "s1" + File.separator + SUBMISSION_ID); @@ -454,7 +503,8 @@ class SubmissionServiceTest { file2.createNewFile(); submissionService.downloadSubmissions(List.of(SUBMISSION_ID, SUBMISSION_ID + 1)); - + verify(submissionRepository).findByCoreSubmissionId(SUBMISSION_ID); + verify(submissionRepository).findByCoreSubmissionId(SUBMISSION_ID+1); verify(fileService).saveFilesToZip(any(), eq(tempStorageDir + File.separator + "downloads" + File.separator + "submissions.zip")); } diff --git a/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java b/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java index feb17eb0ad41dbe7c0eca685585d1643acce0b16..f16d46bb7d0c00a261b081ec96afadad16d9ccca 100644 --- a/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java +++ b/src/test/java/nl/tudelft/submit/service/grading/GradeServiceTest.java @@ -340,6 +340,11 @@ public class GradeServiceTest { .isEqualTo("assignment_1__2__3_bonus_"); } + @Test + public void generateVariableNameNumberInFront() { + assertThat(gradeService.generateVariableName("1_Assignment 1")).isEqualTo("1_assignment_1"); + } + @Test public void passedToFormulaVariable() { GradeSummaryDTO score = new GradeSummaryDTO() diff --git a/src/test/java/nl/tudelft/submit/startup/PublishAllAssignmentsServiceTest.java b/src/test/java/nl/tudelft/submit/startup/PublishAllAssignmentsServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3b02f60ab7fbf02a6aad9f0555b92a32d037d071 --- /dev/null +++ b/src/test/java/nl/tudelft/submit/startup/PublishAllAssignmentsServiceTest.java @@ -0,0 +1,75 @@ +/* + * Submit + * Copyright (C) 2020 - Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.submit.startup; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; + +import nl.tudelft.labracore.lib.api.GradeScheme; +import nl.tudelft.submit.model.SubmitAssignment; +import nl.tudelft.submit.repository.SubmitAssignmentRepository; + +public class PublishAllAssignmentsServiceTest { + + private PublishAllAssignmentsService service; + + @MockBean + private SubmitAssignmentRepository assignmentRepository; + + SubmitAssignment assignment1, assignment2; + + @BeforeEach + void setUp() { + assignmentRepository = Mockito.mock(SubmitAssignmentRepository.class); + service = new PublishAllAssignmentsService(assignmentRepository); + assignment1 = SubmitAssignment.builder() + .assignmentId(1L) + .scoreType(GradeScheme.DUTCH_GRADE) + .hidden(false) + .build(); + + assignment2 = SubmitAssignment.builder() + .assignmentId(2L) + .scoreType(GradeScheme.DUTCH_GRADE) + .hidden(true) + .build(); + } + + @Test + public void testPublishAllJobOffers() { + List<SubmitAssignment> assignments = List.of(assignment1, assignment2); + when(assignmentRepository.findAll()).thenReturn(assignments); + assertTrue(assignment2.getHidden()); + + service.publishAllAssignments(); + + verify(assignmentRepository).findAll(); + + assertFalse(assignment1.getHidden()); + assertFalse(assignment2.getHidden()); + } +} diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml index c83240f28391c5f54945a181948e8368419b6cac..8c13b541f1624fbee4c67ba845b884d6fbe0511c 100644 --- a/src/test/resources/application.yaml +++ b/src/test/resources/application.yaml @@ -34,6 +34,8 @@ submit: timeout: 1800 cache: person-timeout: 60 + startup: + publish-all-assignments: false spring: h2: