Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exam mode: Allow submission upon extension #9833

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -160,6 +167,8 @@ public StudentExamResource(ExamAccessService examAccessService, ExamDeletionServ
this.submissionPolicyRepository = submissionPolicyRepository;
this.examLiveEventsService = examLiveEventsService;
this.examLiveEventRepository = examLiveEventRepository;
this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository;
this.programmingExerciseParticipationService = programmingExerciseParticipationService;
}

/**
Expand Down Expand Up @@ -257,6 +266,21 @@ public ResponseEntity<StudentExam> 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) {
meltemarsl marked this conversation as resolved.
Show resolved Hide resolved
studentExam.getExercises().stream().filter(ProgrammingExercise.class::isInstance).forEach(exercise -> {
meltemarsl marked this conversation as resolved.
Show resolved Hide resolved
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()) {
meltemarsl marked this conversation as resolved.
Show resolved Hide resolved
programmingExerciseStudentParticipation.ifPresent(programmingExerciseParticipationService::unlockStudentRepositoryAndParticipation);
}
});
}
meltemarsl marked this conversation as resolved.
Show resolved Hide resolved
}

return ResponseEntity.ok(savedStudentExam);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -151,6 +156,12 @@ class StudentExamIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabT
@Autowired
private ExamUserRepository examUserRepository;

@Autowired
private ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationTestRepository;

@Autowired
private ProgrammingExerciseTestRepository ProgrammingExerciseTestRepository;

@Autowired
private SubmissionTestRepository submissionRepository;

Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<ExamLiveEventBaseDTO> websocketEventCaptor = ArgumentCaptor.forClass(ExamLiveEventBaseDTO.class);
Expand Down
Loading