Skip to content

Commit

Permalink
Exam mode: Allow instructors to extend exam time (#7119)
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephan Krusche authored Sep 18, 2023
1 parent 881c09b commit a23c6be
Show file tree
Hide file tree
Showing 40 changed files with 885 additions and 128 deletions.
5 changes: 5 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ public final class Constants {
*/
public static final int GROUP_CONVERSATION_HUMAN_READABLE_NAME_LIMIT = 100;

/**
* The name of the topic for notifying the client about changes in the exam working time.
*/
public static final String STUDENT_WORKING_TIME_CHANGE_DURING_CONDUCTION_TOPIC = "/topic/studentExams/%s/working-time-change-during-conduction";

private Constants() {
}
}
9 changes: 9 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/exam/Exam.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.tum.in.www1.artemis.domain.exam;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashSet;
Expand Down Expand Up @@ -192,6 +193,14 @@ public void setEndDate(@NotNull ZonedDateTime endDate) {
this.endDate = endDate;
}

/**
* @return the duration of the exam in seconds
*/
@JsonIgnore
public int getDuration() {
return Math.toIntExact(Duration.between(getStartDate(), getEndDate()).toSeconds());
}

public ZonedDateTime getPublishResultsDate() {
return publishResultsDate;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
import de.tum.in.www1.artemis.service.*;
import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseParticipationService;
import de.tum.in.www1.artemis.service.programming.ProgrammingTriggerService;
import de.tum.in.www1.artemis.service.scheduled.ProgrammingExerciseScheduleService;
import de.tum.in.www1.artemis.service.util.ExamExerciseStartPreparationStatus;
import de.tum.in.www1.artemis.service.util.Tuple;
import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

Expand All @@ -51,8 +53,6 @@ public class StudentExamService {

private static final String EXAM_EXERCISE_START_STATUS_TOPIC = "/topic/exams/%s/exercise-start-status";

private static final String WORKING_TIME_CHANGE_DURING_CONDUCTION_TOPIC = "/topic/studentExams/%s/working-time-change-during-conduction";

private final Logger log = LoggerFactory.getLogger(StudentExamService.class);

private final ParticipationService participationService;
Expand Down Expand Up @@ -89,6 +89,8 @@ public class StudentExamService {

private final WebsocketMessagingService websocketMessagingService;

private final ProgrammingExerciseScheduleService programmingExerciseScheduleService;

private final TaskScheduler scheduler;

public StudentExamService(StudentExamRepository studentExamRepository, UserRepository userRepository, ParticipationService participationService,
Expand All @@ -97,7 +99,7 @@ public StudentExamService(StudentExamRepository studentExamRepository, UserRepos
ProgrammingExerciseParticipationService programmingExerciseParticipationService, SubmissionService submissionService,
StudentParticipationRepository studentParticipationRepository, ExamQuizService examQuizService, ProgrammingExerciseRepository programmingExerciseRepository,
ProgrammingTriggerService programmingTriggerService, ExamRepository examRepository, CacheManager cacheManager, WebsocketMessagingService websocketMessagingService,
@Qualifier("taskScheduler") TaskScheduler scheduler) {
ProgrammingExerciseScheduleService programmingExerciseScheduleService, @Qualifier("taskScheduler") TaskScheduler scheduler) {
this.participationService = participationService;
this.studentExamRepository = studentExamRepository;
this.userRepository = userRepository;
Expand All @@ -115,6 +117,7 @@ public StudentExamService(StudentExamRepository studentExamRepository, UserRepos
this.examRepository = examRepository;
this.cacheManager = cacheManager;
this.websocketMessagingService = websocketMessagingService;
this.programmingExerciseScheduleService = programmingExerciseScheduleService;
this.scheduler = scheduler;
}

Expand Down Expand Up @@ -646,6 +649,13 @@ private void setUpExerciseParticipationsAndSubmissionsWithInitializationDate(Stu
|| ExamDateService.getExamProgrammingExerciseUnlockDate(programmingExercise).isBefore(ZonedDateTime.now())) {
// Note: only unlock the programming exercise student repository for the affected user (Important: Do NOT invoke unlockAll)
programmingExerciseParticipationService.unlockStudentRepositoryAndParticipation(programmingParticipation);

// This is a special case if "prepare exercise start" was pressed shortly before the exam start
// Normally, the locking operation at the end of the exam gets scheduled during the initial unlocking process
// (see ProgrammingExerciseScheduleService#scheduleIndividualRepositoryAndParticipationLockTasks)
// Since this gets never executed here, we need to manually schedule the locking.
var tupel = new Tuple<>(studentExam.getIndividualEndDate(), programmingParticipation);
programmingExerciseScheduleService.scheduleIndividualRepositoryAndParticipationLockTasks(programmingExercise, Set.of(tupel));
}
else {
programmingExerciseParticipationService.lockStudentParticipation(programmingParticipation);
Expand Down Expand Up @@ -786,8 +796,4 @@ private StudentExam generateIndividualStudentExam(Exam exam, User student) {
userHashSet.add(student);
return studentExamRepository.createRandomStudentExams(exam, userHashSet).get(0);
}

public void notifyStudentAboutWorkingTimeChangeDuringConduction(StudentExam studentExam) {
websocketMessagingService.sendMessage(WORKING_TIME_CHANGE_DURING_CONDUCTION_TOPIC.formatted(studentExam.getId()), studentExam.getWorkingTime());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,13 @@ public void sendAssessedExerciseSubmissionNotificationSchedule(Long exerciseId)
}

@Override
public void sendExamWorkingTimeChangeDuringConduction(Long studentExamId) {
public void sendExamWorkingTimeChangeDuringConduction(Long examId) {
log.info("Sending schedule to reschedule exam {} to broker.", examId);
sendMessageDelayed(MessageTopic.EXAM_RESCHEDULE_DURING_CONDUCTION, examId);
}

@Override
public void sendStudentExamIndividualWorkingTimeChangeDuringConduction(Long studentExamId) {
log.info("Sending schedule to reschedule student exam {} to broker.", studentExamId);
sendMessageDelayed(MessageTopic.STUDENT_EXAM_RESCHEDULE_DURING_CONDUCTION, studentExamId);
}
Expand All @@ -162,6 +168,7 @@ public void sendParticipantScoreSchedule(Long exerciseId, Long participantId, Lo
sendMessageDelayed(MessageTopic.PARTICIPANT_SCORE_SCHEDULE, exerciseId, participantId, resultId);
}

// NOTE: Don't remove any of the following methods despite the warning.
private void sendMessageDelayed(MessageTopic topic, Long payload) {
exec.schedule(() -> hazelcastInstance.getTopic(topic.toString()).publish(payload), 1, TimeUnit.SECONDS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,14 @@ public InstanceMessageReceiveService(ProgrammingExerciseRepository programmingEx
SecurityUtils.setAuthorizationObject();
processScheduleAssessedExerciseSubmittedNotification((message.getMessageObject()));
});
hazelcastInstance.<Long>getTopic(MessageTopic.STUDENT_EXAM_RESCHEDULE_DURING_CONDUCTION.toString()).addMessageListener(message -> {
hazelcastInstance.<Long>getTopic(MessageTopic.EXAM_RESCHEDULE_DURING_CONDUCTION.toString()).addMessageListener(message -> {
SecurityUtils.setAuthorizationObject();
processExamWorkingTimeChangeDuringConduction(message.getMessageObject());
});
hazelcastInstance.<Long>getTopic(MessageTopic.STUDENT_EXAM_RESCHEDULE_DURING_CONDUCTION.toString()).addMessageListener(message -> {
SecurityUtils.setAuthorizationObject();
processStudentExamIndividualWorkingTimeChangeDuringConduction(message.getMessageObject());
});
hazelcastInstance.<Long[]>getTopic(MessageTopic.PARTICIPANT_SCORE_SCHEDULE.toString()).addMessageListener(message -> {
SecurityUtils.setAuthorizationObject();
processScheduleParticipantScore(message.getMessageObject()[0], message.getMessageObject()[1], message.getMessageObject()[2]);
Expand Down Expand Up @@ -286,7 +290,12 @@ public void processScheduleAssessedExerciseSubmittedNotification(Long exerciseId
notificationScheduleService.updateSchedulingForAssessedExercisesSubmissions(exercise);
}

public void processExamWorkingTimeChangeDuringConduction(Long studentExamId) {
public void processExamWorkingTimeChangeDuringConduction(Long examId) {
log.info("Received reschedule of exam during conduction {}", examId);
programmingExerciseScheduleService.rescheduleExamDuringConduction(examId);
}

public void processStudentExamIndividualWorkingTimeChangeDuringConduction(Long studentExamId) {
log.info("Received reschedule of student exam during conduction {}", studentExamId);
programmingExerciseScheduleService.rescheduleStudentExamDuringConduction(studentExamId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,19 @@ public interface InstanceMessageSendService {
*/
void sendAssessedExerciseSubmissionNotificationSchedule(Long exerciseId);

/**
* Send a message to the main server that the working time of an exam was changed during the conduction and rescheduling might be necessary
*
* @param examId the id of the exam that should be scheduled
*/
void sendExamWorkingTimeChangeDuringConduction(Long examId);

/**
* Send a message to the main server that the working time of a student exam was changed during the conduction and rescheduling might be necessary
*
* @param studentExamId the id of the student exam that should be scheduled
*/
void sendExamWorkingTimeChangeDuringConduction(Long studentExamId);
void sendStudentExamIndividualWorkingTimeChangeDuringConduction(Long studentExamId);

/**
* Send a message to the main server that schedules to update the participant score for this exercise/participant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,13 @@ public void sendAssessedExerciseSubmissionNotificationSchedule(Long exerciseId)
}

@Override
public void sendExamWorkingTimeChangeDuringConduction(Long studentExamId) {
instanceMessageReceiveService.processExamWorkingTimeChangeDuringConduction(studentExamId);
public void sendExamWorkingTimeChangeDuringConduction(Long examId) {
instanceMessageReceiveService.processExamWorkingTimeChangeDuringConduction(examId);
}

@Override
public void sendStudentExamIndividualWorkingTimeChangeDuringConduction(Long studentExamId) {
instanceMessageReceiveService.processStudentExamIndividualWorkingTimeChangeDuringConduction(studentExamId);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum MessageTopic {
USER_MANAGEMENT_CANCEL_REMOVE_NON_ACTIVATED_USERS("user-management-cancel-remove-non-activated-users"),
EXERCISE_RELEASED_SCHEDULE("exercise-released-schedule"),
ASSESSED_EXERCISE_SUBMISSION_SCHEDULE("assessed-exercise-submission-schedule"),
EXAM_RESCHEDULE_DURING_CONDUCTION("exam-reschedule-during-conduction"),
STUDENT_EXAM_RESCHEDULE_DURING_CONDUCTION("student-exam-reschedule-during-conduction"),
PARTICIPANT_SCORE_SCHEDULE("participant-score-schedule");
// @formatter:on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import de.tum.in.www1.artemis.domain.enumeration.AssessmentType;
import de.tum.in.www1.artemis.domain.enumeration.ExerciseLifecycle;
import de.tum.in.www1.artemis.domain.modeling.ModelingExercise;
import de.tum.in.www1.artemis.repository.*;
import de.tum.in.www1.artemis.repository.ModelingExerciseRepository;
import de.tum.in.www1.artemis.security.SecurityUtils;
import de.tum.in.www1.artemis.service.compass.CompassService;
import de.tum.in.www1.artemis.service.exam.ExamDateService;
Expand Down Expand Up @@ -128,9 +128,7 @@ private void scheduleCourseExercise(ModelingExercise exercise) {

// For any course exercise that needsToBeScheduled (buildAndTestAfterDueDate and/or manual assessment)
if (exercise.getDueDate() != null && ZonedDateTime.now().isBefore(exercise.getDueDate())) {
scheduleService.scheduleTask(exercise, ExerciseLifecycle.DUE, () -> {
buildModelingClusters(exercise).run();
});
scheduleService.scheduleTask(exercise, ExerciseLifecycle.DUE, () -> buildModelingClusters(exercise).run());
log.debug("Scheduled build modeling clusters after due date for Modeling Exercise '{}' (#{}) for {}.", exercise.getTitle(), exercise.getId(), exercise.getDueDate());
}
else {
Expand All @@ -148,9 +146,7 @@ private void scheduleExamExercise(ModelingExercise exercise) {
if (ZonedDateTime.now().isBefore(examDateService.getLatestIndividualExamEndDateWithGracePeriod(exam))) {
var buildDate = endDate.plusMinutes(EXAM_END_WAIT_TIME_FOR_COMPASS_MINUTES);
exercise.setClusterBuildDate(buildDate);
scheduleService.scheduleTask(exercise, ExerciseLifecycle.BUILD_COMPASS_CLUSTERS_AFTER_EXAM, () -> {
buildModelingClusters(exercise).run();
});
scheduleService.scheduleTask(exercise, ExerciseLifecycle.BUILD_COMPASS_CLUSTERS_AFTER_EXAM, () -> buildModelingClusters(exercise).run());
}
log.debug("Scheduled Exam Modeling Exercise '{}' (#{}).", exercise.getTitle(), exercise.getId());
}
Expand All @@ -166,7 +162,7 @@ public void scheduleExerciseForInstant(ModelingExercise exercise) {

/**
* Returns a runnable that, once executed, will build modeling clusters
*
* <p>
* NOTE: this will not build modeling clusters as only a Runnable is returned!
*
* @param exercise The exercise for which the clusters will be created
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.tum.in.www1.artemis.service.scheduled;

import java.time.ZonedDateTime;
import java.util.*;
import java.util.Set;

import javax.annotation.PostConstruct;

Expand Down
Loading

0 comments on commit a23c6be

Please sign in to comment.