Skip to content

Commit

Permalink
Exam mode: Change test case default visibility to after results relea…
Browse files Browse the repository at this point in the history
…se date (#8451)
  • Loading branch information
florian-glombik authored May 18, 2024
1 parent eaa8e67 commit 11fc674
Show file tree
Hide file tree
Showing 42 changed files with 440 additions and 71 deletions.
2 changes: 1 addition & 1 deletion docs/dev/cypress.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Binary file modified docs/user/exams/instructor/add_exercises.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion docs/user/exams/instructors_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* <br>
* Mapping:
* - text: STATIC_CODE_ANALYSIS_FEEDBACK_IDENTIFIER
* - reference: Tool
Expand Down Expand Up @@ -353,10 +353,12 @@ public void setTestCaseType(Set<ProgrammingExerciseTestCase> testCases, Programm
}

private Set<ProgrammingExerciseTestCase> 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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -65,11 +70,13 @@ public class ProgrammingExerciseImportService {

private final ProgrammingExerciseImportBasicService programmingExerciseImportBasicService;

private final ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository;

public ProgrammingExerciseImportService(Optional<VersionControlService> versionControlService, Optional<ContinuousIntegrationService> continuousIntegrationService,
Optional<ContinuousIntegrationTriggerService> 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;
Expand All @@ -82,6 +89,7 @@ public ProgrammingExerciseImportService(Optional<VersionControlService> versionC
this.uriService = uriService;
this.templateUpgradePolicyService = templateUpgradePolicyService;
this.programmingExerciseImportBasicService = programmingExerciseImportBasicService;
this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository;
}

/**
Expand Down Expand Up @@ -271,14 +279,15 @@ private void adjustProjectName(Map<String, String> 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();
Expand All @@ -292,6 +301,15 @@ public ProgrammingExercise importProgrammingExercise(ProgrammingExercise origina
newProgrammingExercise = programmingExerciseImportBasicService.importProgrammingExerciseBasis(originalProgrammingExercise, newProgrammingExercise);
importRepositories(originalProgrammingExercise, newProgrammingExercise);

if (setTestCaseVisibilityToAfterDueDate) {
Set<ProgrammingExerciseTestCase> testCases = this.programmingExerciseTestCaseRepository.findByExerciseId(newProgrammingExercise.getId());
for (ProgrammingExerciseTestCase testCase : testCases) {
testCase.setVisibility(Visibility.AFTER_DUE_DATE);
}
List<ProgrammingExerciseTestCase> updatedTestCases = programmingExerciseTestCaseRepository.saveAll(testCases);
newProgrammingExercise.setTestCases(new HashSet<>(updatedTestCases));
}

// Update the template files
if (updateTemplate) {
TemplateUpgradeService upgradeService = templateUpgradePolicyService.getUpgradeService(newProgrammingExercise.getProgrammingLanguage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ProgrammingExerciseTestCase> reset(Long exerciseId) {
Set<ProgrammingExerciseTestCase> testCases = this.testCaseRepository.findByExerciseId(exerciseId);
public List<ProgrammingExerciseTestCase> reset(ProgrammingExercise programmingExercise) {
Set<ProgrammingExerciseTestCase> 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<ProgrammingExerciseTestCase> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ProgrammingExercise> 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");
}
Expand All @@ -194,7 +197,7 @@ public ResponseEntity<ProgrammingExercise> 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);

Expand Down Expand Up @@ -241,7 +244,7 @@ public ResponseEntity<ProgrammingExercise> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,13 +122,13 @@ public ResponseEntity<Set<ProgrammingExerciseTestCase>> updateTestCases(@PathVar
@EnforceAtLeastEditor
public ResponseEntity<List<ProgrammingExerciseTestCase>> 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<ProgrammingExerciseTestCase> testCases = programmingExerciseTestCaseService.reset(exerciseId);
List<ProgrammingExerciseTestCase> testCases = programmingExerciseTestCaseService.reset(programmingExercise);
return ResponseEntity.ok(testCases);
}
}
Loading

0 comments on commit 11fc674

Please sign in to comment.