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: Prepare exercise start when generating student exams #9119

Open
wants to merge 38 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1212038
prepare exercise start when student exams are generated instead of do…
coolchock Jul 21, 2024
2e585c2
remove start-exercises endpoint
coolchock Jul 21, 2024
8fdc911
remove startExercises() method from student-exams.component.ts
coolchock Jul 21, 2024
e37ccd4
remove prepare exercise start button from student-exams.component.html
coolchock Jul 21, 2024
7f12c2e
remove startExercises method from exam-management.service.ts
coolchock Jul 21, 2024
75e7919
refactor StudentExamService
coolchock Jul 21, 2024
17184b5
adjust ExamQuizServiceTest
coolchock Jul 21, 2024
e2912ec
adjust ExamParticipationIntegrationTest
coolchock Jul 21, 2024
bf42437
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Jul 25, 2024
27ddcf2
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Aug 31, 2024
de21287
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Sep 6, 2024
3985084
fix ExamStartTest^
coolchock Sep 6, 2024
fb4bba7
fix tests
coolchock Sep 6, 2024
29734e8
fix tests
coolchock Sep 6, 2024
fbda235
fix server tests
coolchock Sep 6, 2024
e809b89
fix ProgrammingExamIntegrationTest
coolchock Sep 6, 2024
c6d69f1
remove prepareExerciseStartForExam from ts modules
coolchock Sep 6, 2024
30d1e61
remove start-exercises from exam-management.service.spec.ts
coolchock Sep 7, 2024
a89bd66
fix tests
coolchock Sep 7, 2024
99b075f
remove empty line
coolchock Sep 7, 2024
f670cf7
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Sep 7, 2024
d5764c7
remove unused Javadoc tag
coolchock Sep 7, 2024
6443943
fix test
coolchock Sep 7, 2024
57d1093
remove test
coolchock Sep 7, 2024
4f01e8e
replace assertion with await
coolchock Sep 7, 2024
52d4e84
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Sep 12, 2024
15c1a2b
resolve conflict
coolchock Sep 12, 2024
07fafaf
remove unused translations
coolchock Sep 12, 2024
11a11f3
adjust instructors_guide.rst
coolchock Sep 12, 2024
e7bdede
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Sep 13, 2024
c083f82
update screenshots
coolchock Sep 13, 2024
00f1b77
remove unused keys
coolchock Sep 13, 2024
e234816
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Sep 15, 2024
fc38c66
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Oct 1, 2024
74a98da
resolve merge conflits
coolchock Oct 1, 2024
ae868c5
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Oct 13, 2024
e2437cb
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Oct 20, 2024
33e0ab6
Merge branch 'refs/heads/develop' into feature/exam-mode/start-exam-e…
coolchock Nov 10, 2024
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
Binary file modified docs/user/exams/instructor/student_exams.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 1 addition & 3 deletions docs/user/exams/instructors_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,8 @@ During the exam creation and configuration, you can create your exam and configu
|generate_individual_exams| button will be locked once the exam becomes visible to the students. You cannot perform changes to student exams once the :ref:`exam conduction <exam_conduction>` has started.

- If you have added more students recently, you can choose to |generate_missing_exams|.
- |prepare_exercise_start| creates a participation for each exercise for every registered user, based on their assigned exercises. It also creates the individual repositories and build plans for programming exercises. This action can take a while if there are many registered students due to the communication between the version control (VC) and continuous integration (CI) server.
- Both |generate_individual_exams| and |generate_missing_exams| create a participation for each exercise for every registered user, based on their assigned exercises. It also creates the individual repositories and build plans for programming exercises. This action can take a while if there are many registered students due to the communication between the version control (VC) and continuous integration (CI) server.

.. warning::
You must trigger |prepare_exercise_start| before the :ref:`exam conduction <exam_conduction>` begins.

- On the *Student Exams* page, you can also maintain the repositories of student exams. This functionality only affects programming exercises. You can choose to |lock_repo| and |unlock_repo| all student repositories.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
import de.tum.cit.aet.artemis.communication.service.WebsocketMessagingService;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.SecurityUtils;
import de.tum.cit.aet.artemis.core.service.messaging.InstanceMessageSendService;
import de.tum.cit.aet.artemis.core.util.ExamExerciseStartPreparationStatus;
import de.tum.cit.aet.artemis.exam.domain.Exam;
import de.tum.cit.aet.artemis.exam.domain.StudentExam;
Expand Down Expand Up @@ -125,13 +127,17 @@ public class StudentExamService {

private final ExamQuizQuestionsGenerator examQuizQuestionsGenerator;

private final ExamService examService;

private final InstanceMessageSendService instanceMessageSendService;

public StudentExamService(StudentExamRepository studentExamRepository, UserRepository userRepository, ParticipationService participationService,
QuizSubmissionRepository quizSubmissionRepository, SubmittedAnswerRepository submittedAnswerRepository, TextSubmissionRepository textSubmissionRepository,
ModelingSubmissionRepository modelingSubmissionRepository, SubmissionVersionService submissionVersionService,
ProgrammingExerciseParticipationService programmingExerciseParticipationService, SubmissionService submissionService,
StudentParticipationRepository studentParticipationRepository, ExamQuizService examQuizService, ProgrammingExerciseRepository programmingExerciseRepository,
ProgrammingTriggerService programmingTriggerService, ExamRepository examRepository, CacheManager cacheManager, WebsocketMessagingService websocketMessagingService,
@Qualifier("taskScheduler") TaskScheduler scheduler, QuizPoolService quizPoolService) {
@Qualifier("taskScheduler") TaskScheduler scheduler, QuizPoolService quizPoolService, ExamService examService, InstanceMessageSendService instanceMessageSendService) {
this.participationService = participationService;
this.studentExamRepository = studentExamRepository;
this.userRepository = userRepository;
Expand All @@ -151,6 +157,8 @@ public StudentExamService(StudentExamRepository studentExamRepository, UserRepos
this.websocketMessagingService = websocketMessagingService;
this.scheduler = scheduler;
this.examQuizQuestionsGenerator = quizPoolService;
this.examService = examService;
this.instanceMessageSendService = instanceMessageSendService;
}

/**
Expand Down Expand Up @@ -707,42 +715,76 @@ private void setUpExerciseParticipationsAndSubmissionsWithInitializationDate(Stu
/**
* Starts all the exercises of all the student exams of an exam
*
* @param examId exam to which the student exams belong
* @return a future that will yield the number of generated participations
* @param exam exam to which the generated student exams belong
* @param generatedStudentExams list of student exams generated for this exam
*/
public CompletableFuture<Integer> startExercises(Long examId) {
var exam = examRepository.findWithStudentExamsExercisesById(examId).orElseThrow(() -> new EntityNotFoundException("Exam", examId));
var studentExams = exam.getStudentExams();
public void startExercises(Exam exam, List<StudentExam> generatedStudentExams) {
coolchock marked this conversation as resolved.
Show resolved Hide resolved

long start = System.nanoTime();
if (exam.isTestExam()) {
throw new BadRequestAlertException("Start exercises is only allowed for real exams", "StudentExam", "startExerciseOnlyForRealExams");
}
coolchock marked this conversation as resolved.
Show resolved Hide resolved

examService.combineTemplateCommitsOfAllProgrammingExercisesInExam(exam);

var examId = exam.getId();
List<StudentParticipation> generatedParticipations = Collections.synchronizedList(new ArrayList<>());

int finishedExamsCountInitialValue = 0;
int failedExamsCountInitialValue = 0;
int participationsCountInitialValue = 0;
int studentExamsCountInitialValue = 0;
var cache = cacheManager.getCache(EXAM_EXERCISE_START_STATUS);
if (cache != null) {
var oldValue = cache.get(examId);
if (oldValue != null) {
var oldStatus = (ExamExerciseStartPreparationStatus) oldValue.get();
if (oldStatus != null) {
finishedExamsCountInitialValue = oldStatus.finished();
failedExamsCountInitialValue = oldStatus.failed();
participationsCountInitialValue = oldStatus.participationCount();
studentExamsCountInitialValue = oldStatus.overall();
}
}
cache.evict(examId);
}

var finishedExamsCounter = new AtomicInteger(0);
var failedExamsCounter = new AtomicInteger(0);
var finishedExamsCounter = new AtomicInteger(finishedExamsCountInitialValue);
var failedExamsCounter = new AtomicInteger(failedExamsCountInitialValue);
int participationsCount = participationsCountInitialValue;
var startedAt = ZonedDateTime.now();
var lock = new ReentrantLock();
sendAndCacheExercisePreparationStatus(examId, 0, 0, studentExams.size(), 0, startedAt, lock);

int studentExamsCount = generatedStudentExams.size() + studentExamsCountInitialValue;
sendAndCacheExercisePreparationStatus(examId, finishedExamsCounter.get(), failedExamsCounter.get(), studentExamsCount, participationsCountInitialValue, startedAt, lock);

var threadPool = Executors.newFixedThreadPool(10);
var futures = studentExams.stream()
var futures = generatedStudentExams.stream()
.map(studentExam -> CompletableFuture.runAsync(() -> setUpExerciseParticipationsAndSubmissions(studentExam, generatedParticipations), threadPool)
.thenRun(() -> sendAndCacheExercisePreparationStatus(examId, finishedExamsCounter.incrementAndGet(), failedExamsCounter.get(), studentExams.size(),
generatedParticipations.size(), startedAt, lock))
.thenRun(() -> sendAndCacheExercisePreparationStatus(examId, finishedExamsCounter.incrementAndGet(), failedExamsCounter.get(), studentExamsCount,
generatedParticipations.size() + participationsCount, startedAt, lock))
.exceptionally(throwable -> {
log.error("Exception while preparing exercises for student exam {}", studentExam.getId(), throwable);
sendAndCacheExercisePreparationStatus(examId, finishedExamsCounter.get(), failedExamsCounter.incrementAndGet(), studentExams.size(),
generatedParticipations.size(), startedAt, lock);
sendAndCacheExercisePreparationStatus(examId, finishedExamsCounter.get(), failedExamsCounter.incrementAndGet(), studentExamsCount,
generatedParticipations.size() + participationsCount, startedAt, lock);
return null;
}))
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(futures).thenApply((emtpy) -> {

CompletableFuture.allOf(futures).thenApply((empty) -> {
threadPool.shutdown();
sendAndCacheExercisePreparationStatus(examId, finishedExamsCounter.get(), failedExamsCounter.get(), studentExams.size(), generatedParticipations.size(), startedAt,
lock);
sendAndCacheExercisePreparationStatus(examId, finishedExamsCounter.get(), failedExamsCounter.get(), studentExamsCount,
generatedParticipations.size() + participationsCount, startedAt, lock);
return generatedParticipations.size();
}).thenAccept(numberOfGeneratedParticipations -> {
log.info("Generated {} participations in {} for student exams of exam {}", numberOfGeneratedParticipations, formatDurationFrom(start), examId);
if (ZonedDateTime.now().isAfter(ExamDateService.getExamProgrammingExerciseUnlockDate(exam))) {
// This is a special case if "prepare exercise start" was pressexd 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.
instanceMessageSendService.sendRescheduleAllStudentExams(examId);
}
});
}

Expand Down Expand Up @@ -832,7 +874,13 @@ public List<StudentExam> generateStudentExams(final Exam exam) {
Set<User> users = exam.getRegisteredUsers();

// StudentExams are saved in the called method
return studentExamRepository.createRandomStudentExams(exam, users, examQuizQuestionsGenerator);
var generatedStudentExams = studentExamRepository.createRandomStudentExams(exam, users, examQuizQuestionsGenerator);
var cache = cacheManager.getCache(EXAM_EXERCISE_START_STATUS);
if (cache != null) {
cache.evict(exam.getId());
}
startExercises(exam, generatedStudentExams);
return generatedStudentExams;
}

/**
Expand All @@ -855,6 +903,8 @@ public List<StudentExam> generateMissingStudentExams(Exam exam) {
missingUsers.removeAll(usersWithStudentExam);

// StudentExams are saved in the called method
return studentExamRepository.createRandomStudentExams(exam, missingUsers, examQuizQuestionsGenerator);
var generatedStudentExams = studentExamRepository.createRandomStudentExams(exam, missingUsers, examQuizQuestionsGenerator);
startExercises(exam, generatedStudentExams);
return generatedStudentExams;
coolchock marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -676,45 +676,6 @@ public ResponseEntity<Void> deleteTestRun(@PathVariable Long courseId, @PathVari
return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, testRunId.toString())).build();
}

/**
* POST /courses/{courseId}/exams/{examId}/student-exams/start-exercises : Generate the participation objects
* for all the student exams belonging to the exam
*
* @param courseId the course to which the exam belongs to
* @param examId the exam to which the student exam belongs to
* @return ResponseEntity containing the list of generated participations
*/
@PostMapping(value = "courses/{courseId}/exams/{examId}/student-exams/start-exercises")
@EnforceAtLeastInstructor
public ResponseEntity<Void> startExercises(@PathVariable Long courseId, @PathVariable Long examId) {
long start = System.nanoTime();
examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);
final Exam exam = examRepository.findByIdWithExamUsersExerciseGroupsAndExercisesElseThrow(examId);

if (exam.isTestExam()) {
throw new BadRequestAlertException("Start exercises is only allowed for real exams", "StudentExam", "startExerciseOnlyForRealExams");
}

examService.combineTemplateCommitsOfAllProgrammingExercisesInExam(exam);

User instructor = userRepository.getUser();
log.info("REST request to start exercises for student exams of exam {}", examId);
AuditEvent auditEvent = new AuditEvent(instructor.getLogin(), Constants.PREPARE_EXERCISE_START, "examId=" + examId, "user=" + instructor.getLogin());
auditEventRepository.add(auditEvent);

studentExamService.startExercises(examId).thenAccept(numberOfGeneratedParticipations -> {
log.info("Generated {} participations in {} for student exams of exam {}", numberOfGeneratedParticipations, formatDurationFrom(start), examId);
if (ZonedDateTime.now().isAfter(ExamDateService.getExamProgrammingExerciseUnlockDate(exam))) {
// 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.
instanceMessageSendService.sendRescheduleAllStudentExams(examId);
}
});
return ResponseEntity.ok().build();
}

/**
* GET /courses/{courseId}/exams/{examId}/student-exams/start-exercises/status : Return the current status of
* starting exams for student exams in the given exam if available
Expand Down
9 changes: 0 additions & 9 deletions src/main/webapp/app/exam/manage/exam-management.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,15 +368,6 @@ export class ExamManagementService {
return this.http.post<any>(`${this.resourceUrl}/${courseId}/exams/${examId}/generate-missing-student-exams`, {}, { observe: 'response' });
}

/**
* Start all the exercises for all the student exams belonging to the exam
* @param courseId course to which the exam belongs
* @param examId exam to which the student exams belong
*/
startExercises(courseId: number, examId: number): Observable<HttpResponse<void>> {
return this.http.post<void>(`${this.resourceUrl}/${courseId}/exams/${examId}/student-exams/start-exercises`, {}, { observe: 'response' });
}

/**
* Get the current progress of starting exercises for all students
* @param courseId course to which the exam belongs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ <h2>
}
<span jhiTranslate="artemisApp.studentExams.generateMissingStudentExams"></span>
</button>
<button id="startExercisesButton" class="btn btn-primary mt-1" (click)="startExercises()" [disabled]="isLoading || isExamStarted || exercisePreparationRunning">
coolchock marked this conversation as resolved.
Show resolved Hide resolved
@if (isLoading) {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
}
<span jhiTranslate="artemisApp.studentExams.startExercises"></span>
coolchock marked this conversation as resolved.
Show resolved Hide resolved
</button>
}
}
@if (!localVCEnabled && course?.isAtLeastInstructor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,23 +192,6 @@ export class StudentExamsComponent implements OnInit, OnDestroy {
});
}

/**
* Starts all the exercises of the student exams that belong to the exam
*/
startExercises() {
this.isLoading = true;
this.examManagementService.startExercises(this.courseId, this.examId).subscribe({
next: () => {
this.alertService.success('artemisApp.studentExams.startExerciseSuccess');
coolchock marked this conversation as resolved.
Show resolved Hide resolved
this.isLoading = false;
},
error: (err: HttpErrorResponse) => {
this.handleError('artemisApp.studentExams.startExerciseFailure', err);
this.isLoading = false;
},
});
}

setExercisePreparationStatus(newStatus?: ExamExerciseStartPreparationStatus) {
this.exercisePreparationStatus = newStatus;
const processedExams = (newStatus?.finished ?? 0) + (newStatus?.failed ?? 0);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions src/main/webapp/i18n/de/exam.json
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,6 @@
"title": "Klausuren",
"generateStudentExams": "Individuelle Klausuren erstellen",
"generateMissingStudentExams": "Fehlende individuelle Klausuren erstellen",
"startExercises": "Aufgabenstart vorbereiten",
"evaluateQuizExercises": "Quizzes auswerten",
"assessUnsubmittedStudentExams": "Nicht eingereichte Klausuren bewerten",
"unlockAllRepositories": "Alle Repositories entsperren",
Expand Down Expand Up @@ -392,8 +391,6 @@
"studentExamGenerationError": "Es gab einen Fehler bei der Generierung der individuellen Klausuren:\n {{message}}",
"missingStudentExamGenerationSuccess": "{{number}} fehlende individuelle Klausuren erfolgreich generiert!",
"missingStudentExamGenerationError": "Es gab einen Fehler bei der Generierung der fehlenden individuellen Klausuren:\n {{message}}",
"startExerciseSuccess": "Die Aufgaben werden nun vorbereitet. Dies kann eine Weile dauern.",
"startExerciseFailure": "Es gab einen Fehler bei der Vorbereitung der Aufgaben:\n {{message}}",
"evaluateQuizExerciseSuccess": "{{number}} Quiz-Aufgaben erfolgreich ausgewertet!",
"evaluateQuizExerciseFailure": "Es gab einen Fehler bei der Auswertung der Quiz-Aufgaben:\n {{message}}",
"assessUnsubmittedStudentExamsSuccess": "Alle Klausuren der Studierenden überprüft und die nicht eingereichten oder leeren Modellierungs- und Textaufgaben mit 0 Punkten bewertet.",
Expand Down
3 changes: 0 additions & 3 deletions src/main/webapp/i18n/en/exam.json
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,6 @@
"title": "Student exams",
"generateStudentExams": "Generate individual exams",
"generateMissingStudentExams": "Generate missing individual exams",
"startExercises": "Prepare exercise start",
"evaluateQuizExercises": "Evaluate quizzes",
"assessUnsubmittedStudentExams": "Assess unsubmitted exams",
"unlockAllRepositories": "Unlock all repositories",
Expand Down Expand Up @@ -392,8 +391,6 @@
"studentExamGenerationError": "There was an error during student exam generation:\n {{message}}",
"missingStudentExamGenerationSuccess": "{{number}} missing student exams successfully generated!",
"missingStudentExamGenerationError": "There was an error during the missing student exam generation:\n {{message}}",
"startExerciseSuccess": "The exercises will now be prepared. This may take a while.",
"startExerciseFailure": "There was an error during the preparation of the exercises:\n {{message}}",
"studentExamGenerationModalText": "Some student exams already exist. If you continue, those individual exams will be deleted and new student exams generated.\n\nNote that existing participations and submissions will also be removed!",
"studentExamStatusSuccess": "All registered students have an individual exam",
"studentExamStatusWarning": "Not all registered students have an individual exam",
Expand Down
Loading
Loading