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 @@