diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java index 181b6b8205b1..624e241fa893 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/StudentExamResource.java @@ -73,7 +73,9 @@ import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.cit.aet.artemis.programming.repository.SubmissionPolicyRepository; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseParticipationService; import de.tum.cit.aet.artemis.quiz.repository.SubmittedAnswerRepository; /** @@ -126,11 +128,15 @@ public class StudentExamResource { private static final boolean IS_TEST_RUN = false; + private final ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository; + @Value("${info.student-exam-store-session-data:#{true}}") private boolean storeSessionDataInStudentExamSession; private static final String ENTITY_NAME = "studentExam"; + private final ProgrammingExerciseParticipationService programmingExerciseParticipationService; + @Value("${jhipster.clientApp.name}") private String applicationName; @@ -140,7 +146,8 @@ public StudentExamResource(ExamAccessService examAccessService, ExamDeletionServ StudentParticipationRepository studentParticipationRepository, ExamRepository examRepository, SubmittedAnswerRepository submittedAnswerRepository, AuthorizationCheckService authorizationCheckService, ExamService examService, InstanceMessageSendService instanceMessageSendService, WebsocketMessagingService websocketMessagingService, SubmissionPolicyRepository submissionPolicyRepository, ExamLiveEventsService examLiveEventsService, - ExamLiveEventRepository examLiveEventRepository) { + ExamLiveEventRepository examLiveEventRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, + ProgrammingExerciseParticipationService programmingExerciseParticipationService) { this.examAccessService = examAccessService; this.examDeletionService = examDeletionService; this.studentExamService = studentExamService; @@ -160,6 +167,8 @@ public StudentExamResource(ExamAccessService examAccessService, ExamDeletionServ this.submissionPolicyRepository = submissionPolicyRepository; this.examLiveEventsService = examLiveEventsService; this.examLiveEventRepository = examLiveEventRepository; + this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository; + this.programmingExerciseParticipationService = programmingExerciseParticipationService; } /** @@ -257,6 +266,21 @@ public ResponseEntity updateWorkingTime(@PathVariable Long courseId // potentially re-schedule clustering of modeling submissions (in case Compass is active) examService.scheduleModelingExercises(exam); } + boolean wasEndedOriginally = now.isAfter(exam.getEndDate()); + if (!studentExam.isEnded() && wasEndedOriginally) { + studentExam.getExercises().stream().filter(ProgrammingExercise.class::isInstance).forEach(exercise -> { + var programmingExerciseStudentParticipation = programmingExerciseStudentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), + studentExam.getUser().getLogin()); + var programmingExerciseSubmissionPolicy = ((ProgrammingExercise) exercise).getSubmissionPolicy(); + // Unlock if there is no submission policy + // or there is a submission policy, but its limit was not reached yet + var submissionCount = programmingExerciseStudentParticipationRepository + .findAllWithSubmissionsByExerciseIdAndStudentLogin(exercise.getId(), studentExam.getUser().getLogin()).size(); + if (programmingExerciseSubmissionPolicy == null || submissionCount < programmingExerciseSubmissionPolicy.getSubmissionLimit()) { + programmingExerciseStudentParticipation.ifPresent(programmingExerciseParticipationService::unlockStudentRepositoryAndParticipation); + } + }); + } } return ResponseEntity.ok(savedStudentExam); diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java index e20703c6e709..e383c689792f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/StudentExamIntegrationTest.java @@ -12,6 +12,7 @@ import static org.assertj.core.api.Assertions.within; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; @@ -72,6 +73,8 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.core.security.SecurityUtils; +import de.tum.cit.aet.artemis.core.test_repository.CourseTestRepository; +import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.RoundingUtil; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.ExamUser; @@ -114,6 +117,8 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.LockRepositoryPolicy; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.SubmissionPolicy; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseTestService; @@ -151,6 +156,12 @@ class StudentExamIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabT @Autowired private ExamUserRepository examUserRepository; + @Autowired + private ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationTestRepository; + + @Autowired + private ProgrammingExerciseTestRepository ProgrammingExerciseTestRepository; + @Autowired private SubmissionTestRepository submissionRepository; @@ -205,6 +216,12 @@ class StudentExamIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabT @Autowired private GradingScaleUtilService gradingScaleUtilService; + @Autowired + private CourseTestRepository CourseTestRepository; + + @Autowired + private UserUtilService userUtilService; + private User student1; private Course course1; @@ -231,6 +248,8 @@ class StudentExamIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabT private static final int NUMBER_OF_STUDENTS = 2; + private static final int NUMBER_OF_INSTRUCTORS = 1; + private static final boolean IS_TEST_RUN = false; @BeforeEach @@ -841,6 +860,47 @@ void testUpdateWorkingTimeLate() throws Exception { assertThat(capturedEvent.oldWorkingTime()).isEqualTo(oldWorkingTime); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testUpdateWorkingTime_ShouldTriggerUnlock() throws Exception { + ProgrammingExercise programmingExercise = programmingExerciseUtilService.addCourseExamExerciseGroupWithOneProgrammingExercise(); + ProgrammingExerciseTestRepository.save(programmingExercise); + + Course course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); + CourseTestRepository.save(course); + + userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 0, 0, NUMBER_OF_INSTRUCTORS); + User student = userUtilService.getUserByLogin(TEST_PREFIX + "student1"); + + ProgrammingExerciseStudentParticipation participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, student.getLogin()); + programmingExerciseStudentParticipationTestRepository.save(participation); + + Exam exam = programmingExercise.getExam(); + exam.setStartDate(ZonedDateTime.now().minusHours(2)); + exam.setEndDate(ZonedDateTime.now().minusHours(1)); + examRepository.save(exam); + + StudentExam studentExam = new StudentExam(); + studentExam.setUser(student); + studentExam.setExercises(List.of(programmingExercise)); + studentExam.setExam(exam); + studentExam.setTestRun(false); + studentExam.setWorkingTime(1); + studentExamRepository.save(studentExam); + + doNothing().when(programmingExerciseParticipationService).unlockStudentRepositoryAndParticipation(any()); + + int newWorkingTime = 180 * 60; + + StudentExam updatedExam = request.patchWithResponseBody( + "/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/student-exams/" + studentExam.getId() + "/working-time", newWorkingTime, StudentExam.class, + HttpStatus.OK); + + assertThat(updatedExam).isNotNull(); + assertThat(updatedExam.getWorkingTime()).isEqualTo(newWorkingTime); + assertThat(participation.isLocked()).isFalse(); + } + private ExamLiveEventBaseDTO captureExamLiveEventForId(Long studentExamOrExamId, boolean examWide) { // Create an ArgumentCaptor for the WebSocket message ArgumentCaptor websocketEventCaptor = ArgumentCaptor.forClass(ExamLiveEventBaseDTO.class);