diff --git a/docs/dev/cypress.rst b/docs/dev/cypress.rst index 03d4034fc007..c68afc351ec1 100644 --- a/docs/dev/cypress.rst +++ b/docs/dev/cypress.rst @@ -51,7 +51,7 @@ Follow these steps to create your local cypress instance: "studentGroupName": "students", "tutorGroupName": "tutors", "editorGroupName": "editors", - "instructorGroupName": "instructors" + "instructorGroupName": "instructors", "createUsers": false } diff --git a/docs/user/exams/instructor/add_exercises.png b/docs/user/exams/instructor/add_exercises.png index 659e08d438a4..223bff5f3c4c 100644 Binary files a/docs/user/exams/instructor/add_exercises.png and b/docs/user/exams/instructor/add_exercises.png differ diff --git a/docs/user/exams/instructors_guide.rst b/docs/user/exams/instructors_guide.rst index f7af1bbe3639..069cba54571b 100644 --- a/docs/user/exams/instructors_guide.rst +++ b/docs/user/exams/instructors_guide.rst @@ -190,7 +190,10 @@ During the exam creation and configuration, you can create your exam and configu - In the *Configure Grading* screen, you can tweak the ``weight`` of the tests, the ``bonus multiplier`` and add ``bonus points``. - - You can hide tests so that they are not executed during the exam conduction. Students can not receive feedback from hidden tests during the exam conduction. + - You can hide tests so that they are not executed during the exam conduction by setting the test case visibility to `After Release Date of Results.` Students can not receive feedback from hidden tests during the exam conduction. This option is set by default when creating new programming exercises within an exam. + + .. note:: + When importing exercises, the test case visibility of imported exercise will equal the visibility of the original exercise. You can adjust the test case visibility in the import `Assessment` section to be set to `After Release Date of Results`. .. note:: If you hide all tests, the students will only be able to see if their submission compiles during the conduction. Set the ``Run Tests once after Due Date`` after the diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java index 77c81184c29a..2cbed9905590 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java @@ -47,6 +47,7 @@ import de.tum.in.www1.artemis.domain.enumeration.ProjectType; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; +import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.domain.hestia.ExerciseHint; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; import de.tum.in.www1.artemis.domain.participation.Participation; @@ -942,4 +943,15 @@ public String getBuildScript() { public void setBuildScript(String buildScript) { this.buildScript = buildScript; } + + /** + * In course exercises students shall receive immediate feedback. {@link Visibility#ALWAYS} + * In Exams misconfiguration and leaking test results to students during an exam shall be prevented by the default setting. {@link Visibility#AFTER_DUE_DATE} + * + * @return default visibility {@link Visibility} set after the first execution of a test case + * or when resetting the test case settings + */ + public Visibility getDefaultTestCaseVisibility() { + return this.isExamExercise() ? Visibility.AFTER_DUE_DATE : Visibility.ALWAYS; + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java index 0b8caa4f6350..ba38ee1df128 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamImportService.java @@ -319,7 +319,8 @@ private void addExercisesToExerciseGroup(ExerciseGroup exerciseGroupToCopy, Exer originalProgrammingExercise.setTasks(new ArrayList<>(templateTasks)); prepareProgrammingExerciseForExamImport((ProgrammingExercise) exerciseToCopy); - yield Optional.of(programmingExerciseImportService.importProgrammingExercise(originalProgrammingExercise, (ProgrammingExercise) exerciseToCopy, false, false)); + yield Optional + .of(programmingExerciseImportService.importProgrammingExercise(originalProgrammingExercise, (ProgrammingExercise) exerciseToCopy, false, false, false)); } case FILE_UPLOAD -> { diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationService.java index 4a87bd518bc3..0fefe43914de 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseFeedbackCreationService.java @@ -160,7 +160,7 @@ private String removeCIDirectoriesFromPath(String sourcePath) { * Transforms static code analysis reports to feedback objects. * As we reuse the Feedback entity to store static code analysis findings, a mapping to those attributes * has to be defined, violating the first normal form. - *

+ *
* Mapping: * - text: STATIC_CODE_ANALYSIS_FEEDBACK_IDENTIFIER * - reference: Tool @@ -353,10 +353,12 @@ public void setTestCaseType(Set testCases, Programm } private Set getTestCasesFromBuildResult(AbstractBuildResultNotificationDTO buildResult, ProgrammingExercise exercise) { + Visibility defaultVisibility = exercise.getDefaultTestCaseVisibility(); + return buildResult.getBuildJobs().stream().flatMap(job -> Stream.concat(job.getFailedTests().stream(), job.getSuccessfulTests().stream())) // we use default values for weight, bonus multiplier and bonus points .map(testCase -> new ProgrammingExerciseTestCase().testName(testCase.getName()).weight(1.0).bonusMultiplier(1.0).bonusPoints(0.0).exercise(exercise).active(true) - .visibility(Visibility.ALWAYS)) + .visibility(defaultVisibility)) .collect(Collectors.toSet()); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java index 6acd53b3f22f..135661c2cf82 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java @@ -6,9 +6,11 @@ import java.io.IOException; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.eclipse.jgit.api.errors.GitAPIException; import org.slf4j.Logger; @@ -20,12 +22,15 @@ import de.tum.in.www1.artemis.domain.AuxiliaryRepository; import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; import de.tum.in.www1.artemis.domain.Repository; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.BuildPlanType; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; +import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; +import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.service.FileService; import de.tum.in.www1.artemis.service.UriService; @@ -65,11 +70,13 @@ public class ProgrammingExerciseImportService { private final ProgrammingExerciseImportBasicService programmingExerciseImportBasicService; + private final ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository; + public ProgrammingExerciseImportService(Optional versionControlService, Optional continuousIntegrationService, Optional continuousIntegrationTriggerService, ProgrammingExerciseService programmingExerciseService, ProgrammingExerciseTaskService programmingExerciseTaskService, GitService gitService, FileService fileService, UserRepository userRepository, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, UriService uriService, TemplateUpgradePolicyService templateUpgradePolicyService, - ProgrammingExerciseImportBasicService programmingExerciseImportBasicService) { + ProgrammingExerciseImportBasicService programmingExerciseImportBasicService, ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository) { this.versionControlService = versionControlService; this.continuousIntegrationService = continuousIntegrationService; this.continuousIntegrationTriggerService = continuousIntegrationTriggerService; @@ -82,6 +89,7 @@ public ProgrammingExerciseImportService(Optional versionC this.uriService = uriService; this.templateUpgradePolicyService = templateUpgradePolicyService; this.programmingExerciseImportBasicService = programmingExerciseImportBasicService; + this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository; } /** @@ -271,14 +279,15 @@ private void adjustProjectName(Map replacements, String projectK * Method to import a programming exercise, including all base build plans (template, solution) and repositories (template, solution, test). * Referenced entities, s.a. the test cases or the hints will get cloned and assigned a new id. * - * @param originalProgrammingExercise the Programming Exercise which should be used as a blueprint - * @param newProgrammingExercise The new exercise already containing values which should not get copied, i.e. overwritten - * @param updateTemplate if the template files should be updated - * @param recreateBuildPlans if the build plans should be recreated + * @param originalProgrammingExercise the Programming Exercise which should be used as a blueprint + * @param newProgrammingExercise The new exercise already containing values which should not get copied, i.e. overwritten + * @param updateTemplate if the template files should be updated + * @param recreateBuildPlans if the build plans should be recreated + * @param setTestCaseVisibilityToAfterDueDate if the test case visibility should be set to {@link Visibility#AFTER_DUE_DATE} * @return the imported programming exercise */ public ProgrammingExercise importProgrammingExercise(ProgrammingExercise originalProgrammingExercise, ProgrammingExercise newProgrammingExercise, boolean updateTemplate, - boolean recreateBuildPlans) throws JsonProcessingException { + boolean recreateBuildPlans, boolean setTestCaseVisibilityToAfterDueDate) throws JsonProcessingException { // remove all non-alphanumeric characters from the short name. This gets already done in the client, but we do it again here to be sure newProgrammingExercise.setShortName(newProgrammingExercise.getShortName().replaceAll("[^a-zA-Z0-9]", "")); newProgrammingExercise.generateAndSetProjectKey(); @@ -292,6 +301,15 @@ public ProgrammingExercise importProgrammingExercise(ProgrammingExercise origina newProgrammingExercise = programmingExerciseImportBasicService.importProgrammingExerciseBasis(originalProgrammingExercise, newProgrammingExercise); importRepositories(originalProgrammingExercise, newProgrammingExercise); + if (setTestCaseVisibilityToAfterDueDate) { + Set testCases = this.programmingExerciseTestCaseRepository.findByExerciseId(newProgrammingExercise.getId()); + for (ProgrammingExerciseTestCase testCase : testCases) { + testCase.setVisibility(Visibility.AFTER_DUE_DATE); + } + List updatedTestCases = programmingExerciseTestCaseRepository.saveAll(testCases); + newProgrammingExercise.setTestCases(new HashSet<>(updatedTestCases)); + } + // Update the template files if (updateTemplate) { TemplateUpgradeService upgradeService = templateUpgradePolicyService.getUpgradeService(newProgrammingExercise.getProgrammingLanguage()); diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseTestCaseService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseTestCaseService.java index a70996c86f12..c6b40bd2ac55 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseTestCaseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseTestCaseService.java @@ -21,7 +21,6 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; import de.tum.in.www1.artemis.domain.User; -import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseTaskService; @@ -125,21 +124,21 @@ private static void validateTestCase(ProgrammingExerciseTestCase testCase) { /** * Reset all tests to their initial configuration * - * @param exerciseId to find exercise test cases + * @param programmingExercise that shall be reset * @return test cases that have been reset */ - public List reset(Long exerciseId) { - Set testCases = this.testCaseRepository.findByExerciseId(exerciseId); + public List reset(ProgrammingExercise programmingExercise) { + Set testCases = this.testCaseRepository.findByExerciseId(programmingExercise.getId()); for (ProgrammingExerciseTestCase testCase : testCases) { testCase.setWeight(1.0); testCase.setBonusMultiplier(1.0); testCase.setBonusPoints(0.0); - testCase.setVisibility(Visibility.ALWAYS); + testCase.setVisibility(programmingExercise.getDefaultTestCaseVisibility()); } List updatedTestCases = testCaseRepository.saveAll(testCases); // The tests' weights were updated. We use this flag to inform the instructor about outdated student results. - programmingTriggerService.setTestCasesChangedAndTriggerTestCaseUpdate(exerciseId); + programmingTriggerService.setTestCasesChangedAndTriggerTestCaseUpdate(programmingExercise.getId()); return updatedTestCases; } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java index 6e0e1bd3a1b1..21c0de2c44b9 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseExportImportResource.java @@ -45,6 +45,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; +import de.tum.in.www1.artemis.domain.enumeration.Visibility; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; @@ -166,21 +167,23 @@ private void validateStaticCodeAnalysisSettings(ProgrammingExercise programmingE * This will import the whole exercise, including all base build plans (template, solution) and repositories * (template, solution, test). Referenced entities, s.a. the test cases or the hints will get cloned and assigned * a new id. For a concrete list of what gets copied and what not have a look - * at {@link ProgrammingExerciseImportService#importProgrammingExercise(ProgrammingExercise, ProgrammingExercise, boolean, boolean)} + * at {@link ProgrammingExerciseImportService#importProgrammingExercise(ProgrammingExercise, ProgrammingExercise, boolean, boolean, boolean)} * - * @param sourceExerciseId The ID of the original exercise which should get imported - * @param newExercise The new exercise containing values that should get overwritten in the imported exercise, s.a. the title or difficulty - * @param recreateBuildPlans Option determining whether the build plans should be copied or re-created from scratch - * @param updateTemplate Option determining whether the template files should be updated with the most recent template version + * @param sourceExerciseId The ID of the original exercise which should get imported + * @param newExercise The new exercise containing values that should get overwritten in the imported exercise, s.a. the title or difficulty + * @param recreateBuildPlans Option determining whether the build plans should be copied or re-created from scratch + * @param updateTemplate Option determining whether the template files should be updated with the most recent template version + * @param setTestCaseVisibilityToAfterDueDate Option determining whether the test case visibility should be set to {@link Visibility#AFTER_DUE_DATE} * @return The imported exercise (200), a not found error (404) if the template does not exist, or a forbidden error * (403) if the user is not at least an instructor in the target course. - * @see ProgrammingExerciseImportService#importProgrammingExercise(ProgrammingExercise, ProgrammingExercise, boolean, boolean) + * @see ProgrammingExerciseImportService#importProgrammingExercise(ProgrammingExercise, ProgrammingExercise, boolean, boolean, boolean) */ @PostMapping("programming-exercises/import/{sourceExerciseId}") @EnforceAtLeastEditor @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity importProgrammingExercise(@PathVariable long sourceExerciseId, @RequestBody ProgrammingExercise newExercise, - @RequestParam(defaultValue = "false") boolean recreateBuildPlans, @RequestParam(defaultValue = "false") boolean updateTemplate) throws JsonProcessingException { + @RequestParam(defaultValue = "false") boolean recreateBuildPlans, @RequestParam(defaultValue = "false") boolean updateTemplate, + @RequestParam(defaultValue = "false") boolean setTestCaseVisibilityToAfterDueDate) throws JsonProcessingException { if (sourceExerciseId < 0) { throw new BadRequestAlertException("Invalid source id when importing programming exercises", ENTITY_NAME, "invalidSourceExerciseId"); } @@ -194,7 +197,7 @@ public ResponseEntity importProgrammingExercise(@PathVariab newExercise.validateSettingsForFeedbackRequest(); validateStaticCodeAnalysisSettings(newExercise); - final var user = userRepository.getUserWithGroupsAndAuthorities(); + final User user = userRepository.getUserWithGroupsAndAuthorities(); Course course = courseService.retrieveCourseOverExerciseGroupOrCourseId(newExercise); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, user); @@ -241,7 +244,7 @@ public ResponseEntity importProgrammingExercise(@PathVariab try { ProgrammingExercise importedProgrammingExercise = programmingExerciseImportService.importProgrammingExercise(originalProgrammingExercise, newExercise, updateTemplate, - recreateBuildPlans); + recreateBuildPlans, setTestCaseVisibilityToAfterDueDate); // remove certain properties which are not relevant for the client to keep the response small importedProgrammingExercise.setTestCases(null); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseTestCaseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseTestCaseResource.java index 48a3f18d77d7..b3194bb0e142 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseTestCaseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseTestCaseResource.java @@ -16,7 +16,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingExerciseTestCase; +import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseTestCaseRepository; import de.tum.in.www1.artemis.repository.UserRepository; @@ -120,13 +122,13 @@ public ResponseEntity> updateTestCases(@PathVar @EnforceAtLeastEditor public ResponseEntity> resetTestCases(@PathVariable Long exerciseId) { log.debug("REST request to reset the test case weights of exercise {}", exerciseId); - var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - var user = userRepository.getUserWithGroupsAndAuthorities(); + ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); + User user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, user); programmingExerciseTestCaseService.logTestCaseReset(user, programmingExercise, programmingExercise.getCourseViaExerciseGroupOrCourseMember()); - List testCases = programmingExerciseTestCaseService.reset(exerciseId); + List testCases = programmingExerciseTestCaseService.reset(programmingExercise); return ResponseEntity.ok(testCases); } } diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html index a3cca888f9bb..c7dcd480e6e8 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html @@ -120,9 +120,7 @@

} @else { - @if (studentExam.submitted) { - - } + } } diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.ts b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.ts index 32cba7073ac4..817b93ffddb9 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.ts +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.ts @@ -64,7 +64,7 @@ export class StudentExamDetailComponent implements OnInit, OnDestroy { this.examId = params.examId; this.courseId = params.courseId; this.setStudentExamWithGrade(data.studentExam); - this.isTestExam = data.studentExam.exam.testExam; + this.isTestExam = data.studentExam.exam?.testExam; this.isTestRun = url[1]?.toString() === 'test-runs'; }); } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component.html b/src/main/webapp/app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component.html index 9a4201087b0b..52934fae6007 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component.html @@ -93,7 +93,10 @@

Tasks