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 instructors to extend exam time #7119

Merged
merged 48 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c6f8c66
add a new REST call to enable time extension of the whole exam
Aug 30, 2023
e309d48
use correct request test method
Aug 30, 2023
3edca95
Merge branch 'develop' into feature/exam-mode/time-extension
Aug 30, 2023
ca8a348
Merge branch 'develop' into feature/exam-mode/time-extension
Aug 31, 2023
bb0aea1
Client implementation
aplr Sep 8, 2023
4d413b3
Update exam state, switch to relative value input, fix editor warning…
aplr Sep 9, 2023
ace83f6
Disable changing working time 5 minutes before exam end
aplr Sep 9, 2023
9652eb7
Add rescheduling of repository lock operations
aplr Sep 9, 2023
e41e989
Update src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java
aplr Sep 9, 2023
934ebc0
Merge commit 'f6e9a37cfc01ebccb4f439ba2cd90e77ddbedd84' into feature/…
aplr Sep 9, 2023
9871da5
Fix warning
aplr Sep 9, 2023
16648df
Self-review changes
aplr Sep 9, 2023
52f078e
Send working time extension instance notification once
aplr Sep 9, 2023
4504450
Teamscale fixes
aplr Sep 9, 2023
f7d357b
Rescheduling for whole exam, grouped by participation due date
aplr Sep 9, 2023
885c6b7
Merge commit '12057ed62683db3897691d395f5f93baf583a05a' into feature/…
aplr Sep 10, 2023
cc6df6b
Revert "Fix warning"
aplr Sep 10, 2023
b566e62
Add note to shadowed methods
aplr Sep 10, 2023
ed6a721
Update src/main/webapp/app/exam/shared/working-time-update/working-ti…
aplr Sep 10, 2023
09bda7a
Update debug log
aplr Sep 10, 2023
e2adaa8
Fix test declaration import
aplr Sep 10, 2023
56d42fd
Fix client tests
aplr Sep 12, 2023
f298f2d
PR feedback
aplr Sep 13, 2023
0c8469d
PR feedback
aplr Sep 13, 2023
6393263
Fix button naming
aplr Sep 13, 2023
f1437e5
PR feedback
aplr Sep 13, 2023
442352b
PR feedback
aplr Sep 13, 2023
c92220a
Remove comment
aplr Sep 13, 2023
9b8b92e
Use event emitter instead of callback
aplr Sep 13, 2023
c0fa9c6
Fix client tests
aplr Sep 13, 2023
4ae05d6
Unsubscribe on destroy
aplr Sep 13, 2023
438cd9a
Merge commit '4332d9e11655ed417350356ef0a3acf2c509df4a' into feature/…
aplr Sep 13, 2023
6b15787
Fix prettier
aplr Sep 14, 2023
26bed1f
improve logic for scheduling modeling exercises when updating (indivi…
Sep 14, 2023
3afaca9
Merge branch 'develop' into feature/exam-mode/time-extension
Strohgelaender Sep 14, 2023
2655403
Fix PR feedback
aplr Sep 14, 2023
5728c11
Fix PR feedback
aplr Sep 14, 2023
44e2402
Fix PR feedback
aplr Sep 14, 2023
67bbdab
Fetch exercise groups
aplr Sep 14, 2023
8f47b7e
Next checkout timeout
aplr Sep 14, 2023
9459fe1
Merge branch 'develop' of https://github.com/ls1intum/Artemis into fe…
Strohgelaender Sep 15, 2023
c3a4f28
fix an issue with individual end dates
Strohgelaender Sep 15, 2023
bb9f3a0
fix an issue with time extensions
Strohgelaender Sep 15, 2023
b4d537c
Code cleanup
aplr Sep 16, 2023
73adf8e
Update note
aplr Sep 16, 2023
df245cf
update note
Strohgelaender Sep 16, 2023
46f142c
Merge branch 'develop' into feature/exam-mode/time-extension
Strohgelaender Sep 16, 2023
a69f318
Display absolute working time
aplr Sep 16, 2023
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
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 @@ -51,8 +51,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 @@ -786,8 +784,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 sendStudentExamWorkingTimeChangeDuringConduction(Long studentExamId) {
aplr marked this conversation as resolved.
Show resolved Hide resolved
log.info("Sending schedule to reschedule student exam {} to broker.", studentExamId);
sendMessageDelayed(MessageTopic.STUDENT_EXAM_RESCHEDULE_DURING_CONDUCTION, studentExamId);
}
Expand All @@ -162,10 +168,6 @@ public void sendParticipantScoreSchedule(Long exerciseId, Long participantId, Lo
sendMessageDelayed(MessageTopic.PARTICIPANT_SCORE_SCHEDULE, exerciseId, participantId, resultId);
}

private void sendMessageDelayed(MessageTopic topic, Long payload) {
exec.schedule(() -> hazelcastInstance.getTopic(topic.toString()).publish(payload), 1, TimeUnit.SECONDS);
}

Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
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();
processStudentExamWorkingTimeChangeDuringConduction(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 processStudentExamWorkingTimeChangeDuringConduction(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 sendStudentExamWorkingTimeChangeDuringConduction(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 sendStudentExamWorkingTimeChangeDuringConduction(Long studentExamId) {
instanceMessageReceiveService.processStudentExamWorkingTimeChangeDuringConduction(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
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.*;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
Expand Down Expand Up @@ -74,6 +71,8 @@ public class ProgrammingExerciseScheduleService implements IExerciseScheduleServ

private final GroupNotificationService groupNotificationService;

private final ExamRepository examRepository;

private final StudentExamRepository studentExamRepository;

private final ExamDateService examDateService;
Expand All @@ -84,8 +83,8 @@ public ProgrammingExerciseScheduleService(ScheduleService scheduleService, Progr
ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository, ResultRepository resultRepository, ParticipationRepository participationRepository,
ProgrammingExerciseStudentParticipationRepository programmingExerciseParticipationRepository, Environment env, ProgrammingTriggerService programmingTriggerService,
ProgrammingExerciseGradingService programmingExerciseGradingService, GroupNotificationService groupNotificationService, ExamDateService examDateService,
ProgrammingExerciseParticipationService programmingExerciseParticipationService, ExerciseDateService exerciseDateService, StudentExamRepository studentExamRepository,
GitService gitService) {
ProgrammingExerciseParticipationService programmingExerciseParticipationService, ExerciseDateService exerciseDateService, ExamRepository examRepository,
StudentExamRepository studentExamRepository, GitService gitService) {
this.scheduleService = scheduleService;
this.programmingExerciseRepository = programmingExerciseRepository;
this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository;
Expand All @@ -95,6 +94,7 @@ public ProgrammingExerciseScheduleService(ScheduleService scheduleService, Progr
this.programmingTriggerService = programmingTriggerService;
this.groupNotificationService = groupNotificationService;
this.exerciseDateService = exerciseDateService;
this.examRepository = examRepository;
this.studentExamRepository = studentExamRepository;
this.examDateService = examDateService;
this.programmingExerciseParticipationService = programmingExerciseParticipationService;
Expand Down Expand Up @@ -802,6 +802,7 @@ private void scheduleIndividualRepositoryAndParticipationLockTasks(ProgrammingEx
// 1. Group all participations by due date (TODO use student exams for safety if some participations are not pre-generated)
var participationsGroupedByDueDate = individualDueDates.stream().filter(tuple -> tuple.x() != null)
.collect(Collectors.groupingBy(Tuple::x, Collectors.mapping(Tuple::y, Collectors.toSet())));

// 2. Transform those groups into lock-repository tasks with times
Set<Tuple<ZonedDateTime, Runnable>> tasks = participationsGroupedByDueDate.entrySet().stream().map(entry -> {
// Check that this participation is planed to be locked and has still the same due date
Expand All @@ -810,29 +811,51 @@ private void scheduleIndividualRepositoryAndParticipationLockTasks(ProgrammingEx
var task = lockStudentRepositoriesAndParticipations(exercise, lockingCondition);
return new Tuple<>(entry.getKey(), task);
}).collect(Collectors.toSet());

// 3. Schedule all tasks
scheduleService.scheduleTask(exercise, ExerciseLifecycle.DUE, tasks);
}

private void scheduleIndividualRepositoryLockTasks(ProgrammingExercise programmingExercise) {
Optional<StudentParticipation> participation = programmingExercise.getStudentParticipations().stream().findFirst();
if (participation.isEmpty() || !(participation.get() instanceof ProgrammingExerciseStudentParticipation programmingParticipation)) {
return;
}
ZonedDateTime dueDate = studentExamRepository.getIndividualDueDate(programmingExercise, programmingParticipation);

scheduleIndividualRepositoryAndParticipationLockTasks(programmingExercise, Set.of(new Tuple<>(dueDate, programmingParticipation)));
}

/**
* Reschedules all programming exercises in this exam, since the working time was changed
*
* @param examId the id of the exam
*/
public void rescheduleExamDuringConduction(Long examId) {
Exam exam = examRepository.findWithExerciseGroupsExercisesParticipationsAndSubmissionsById(examId).orElseThrow(NoSuchElementException::new);

// get all programming exercises in the exam
List<ProgrammingExercise> programmingExercises = exam.getExerciseGroups().stream().flatMap(exerciseGroup -> exerciseGroup.getExercises().stream())
.filter(exercise -> exercise instanceof ProgrammingExercise).map(exercise -> (ProgrammingExercise) exercise).toList();

// schedule repository locks for each programming exercise, grouped by participation due date
programmingExercises.forEach(this::scheduleIndividualRepositoryLockTasks);
}

/**
* Reschedules all programming exercises in this student exam, since the working time was changed
*
* @param studentExamId the id of the student exam
*/
public void rescheduleStudentExamDuringConduction(Long studentExamId) {
StudentExam studentExam = studentExamRepository.findWithExercisesParticipationsSubmissionsById(studentExamId, false).orElseThrow(NoSuchElementException::new);

// get all programming exercises in the student's exam
List<ProgrammingExercise> programmingExercises = studentExam.getExercises().stream().filter(exercise -> exercise instanceof ProgrammingExercise)
.map(exercise -> (ProgrammingExercise) exercise).toList();

programmingExercises.forEach(programmingExercise -> {
Optional<StudentParticipation> participation = programmingExercise.getStudentParticipations().stream().findFirst();
if (participation.isEmpty() || !(participation.get() instanceof ProgrammingExerciseStudentParticipation programmingParticipation)) {
return;
}
ZonedDateTime dueDate = studentExamRepository.getIndividualDueDate(programmingExercise, programmingParticipation);

scheduleIndividualRepositoryAndParticipationLockTasks(programmingExercise, Set.of(new Tuple<>(dueDate, programmingParticipation)));
});
// schedule repository locks for each programming exercise, grouped by participation due date
programmingExercises.forEach(this::scheduleIndividualRepositoryLockTasks);
}

private CompletableFuture<List<ProgrammingExerciseStudentParticipation>> removeWritePermissionsFromAllStudentRepositoriesAndLockParticipations(Long programmingExerciseId,
Expand Down
Loading