Skip to content

Commit

Permalink
Exam mode: Use quiz pool in student exam generation (#7583)
Browse files Browse the repository at this point in the history
  • Loading branch information
rriyaldhi authored Jan 8, 2024
1 parent 7d294a3 commit 2ea0ab5
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 72 deletions.
24 changes: 24 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/exam/StudentExam.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import javax.persistence.*;

import org.hibernate.Hibernate;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

Expand All @@ -15,6 +16,7 @@
import de.tum.in.www1.artemis.domain.AbstractAuditingEntity;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.quiz.QuizQuestion;

@Entity
@Table(name = "student_exam")
Expand Down Expand Up @@ -63,6 +65,10 @@ public class StudentExam extends AbstractAuditingEntity {
@JsonIgnoreProperties("studentExam")
private Set<ExamSession> examSessions = new HashSet<>();

@ManyToMany
@JoinTable(name = "student_exam_quiz_question", joinColumns = @JoinColumn(name = "student_exam_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "quiz_question_id", referencedColumnName = "id"))
private List<QuizQuestion> quizQuestions = new ArrayList<>();

public Boolean isSubmitted() {
return submitted;
}
Expand Down Expand Up @@ -160,6 +166,24 @@ public void setExamSessions(Set<ExamSession> examSessions) {
this.examSessions = examSessions;
}

/**
* Returns a list of quiz questions associated with the student exam.
* If the quizQuestions list is not null and has been initialized, it returns the list of quiz questions.
* Otherwise, it returns an empty list.
*
* @return the list of quiz questions associated with the student exam
*/
public List<QuizQuestion> getQuizQuestions() {
if (quizQuestions != null && Hibernate.isInitialized(quizQuestions)) {
return quizQuestions;
}
return Collections.emptyList();
}

public void setQuizQuestions(List<QuizQuestion> quizQuestions) {
this.quizQuestions = quizQuestions;
}

/**
* Adds the given exam session to the student exam
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public void setRandomizeQuestionOrder(Boolean randomizeQuestionOrder) {

@Override
public void setQuestionParent(QuizQuestion quizQuestion) {
// Do nothing since the relationship between QuizPool and QuizQuestion is defined in QuizPool.
// Do nothing since the relationship between QuizPool and QuizQuestion is already defined above.
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import de.tum.in.www1.artemis.domain.exam.ExerciseGroup;
import de.tum.in.www1.artemis.domain.exam.StudentExam;
import de.tum.in.www1.artemis.domain.participation.StudentParticipation;
import de.tum.in.www1.artemis.domain.quiz.QuizQuestion;
import de.tum.in.www1.artemis.service.ExerciseDateService;
import de.tum.in.www1.artemis.service.exam.ExamQuizQuestionsGenerator;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

/**
Expand Down Expand Up @@ -376,11 +378,12 @@ default Integer findMaxWorkingTimeByExamIdElseThrow(Long examId) {
/**
* Generates random exams for each user in the given users set and saves them.
*
* @param exam exam for which the individual student exams will be generated
* @param users users for which the individual exams will be generated
* @param exam exam for which the individual student exams will be generated
* @param users users for which the individual exams will be generated
* @param examQuizQuestionsGenerator generator to generate quiz questions for the exam
* @return List of StudentExams generated for the given users
*/
default List<StudentExam> createRandomStudentExams(Exam exam, Set<User> users) {
default List<StudentExam> createRandomStudentExams(Exam exam, Set<User> users, ExamQuizQuestionsGenerator examQuizQuestionsGenerator) {
List<StudentExam> studentExams = new ArrayList<>();
SecureRandom random = new SecureRandom();
long numberOfOptionalExercises = exam.getNumberOfExercisesInExam() - exam.getExerciseGroups().stream().filter(ExerciseGroup::getIsMandatory).count();
Expand Down Expand Up @@ -421,6 +424,8 @@ default List<StudentExam> createRandomStudentExams(Exam exam, Set<User> users) {
if (Boolean.TRUE.equals(exam.getRandomizeExerciseOrder())) {
Collections.shuffle(studentExam.getExercises());
}
List<QuizQuestion> quizQuestions = examQuizQuestionsGenerator.generateQuizQuestionsForExam(exam.getId());
studentExam.setQuizQuestions(quizQuestions);

studentExams.add(studentExam);
}
Expand Down Expand Up @@ -453,39 +458,46 @@ private Exercise selectRandomExercise(SecureRandom random, ExerciseGroup exercis
* Generates the student exams randomly based on the exam configuration and the exercise groups
* Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded)
*
* @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded
* @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded
* @param examQuizQuestionsGenerator generator to generate quiz questions for the exam
* @return the list of student exams with their corresponding users
*/
default List<StudentExam> generateStudentExams(final Exam exam) {
default List<StudentExam> generateStudentExams(final Exam exam, ExamQuizQuestionsGenerator examQuizQuestionsGenerator) {
final var existingStudentExams = findByExamId(exam.getId());
// https://jira.spring.io/browse/DATAJPA-1367 deleteInBatch does not work, because it does not cascade the deletion of existing exam sessions, therefore use deleteAll
deleteAll(existingStudentExams);

Set<User> users = exam.getRegisteredUsers();

// StudentExams are saved in the called method
return createRandomStudentExams(exam, users);
return createRandomStudentExams(exam, users, examQuizQuestionsGenerator);
}

/**
* Generates the missing student exams randomly based on the exam configuration and the exercise groups.
* The difference between all registered users and the users who already have an individual exam is the set of users for which student exams will be created.
* <p>
* Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded)
* Get all student exams for the given exam id with quiz questions.
*
* @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded
* @return the list of student exams with their corresponding users
* @param ids the ids of the student exams
* @return the list of student exams with quiz questions
*/
default List<StudentExam> generateMissingStudentExams(Exam exam) {

// Get all users who already have an individual exam
Set<User> usersWithStudentExam = findUsersWithStudentExamsForExam(exam.getId());

// Get all students who don't have an exam yet
Set<User> missingUsers = exam.getRegisteredUsers();
missingUsers.removeAll(usersWithStudentExam);
@Query("""
SELECT DISTINCT se
FROM StudentExam se
LEFT JOIN FETCH se.quizQuestions qq
WHERE se.id IN :ids
""")
List<StudentExam> findAllWithEagerQuizQuestionsById(List<Long> ids);

// StudentExams are saved in the called method
return createRandomStudentExams(exam, missingUsers);
}
/**
* Get all student exams for the given exam id with exercises.
*
* @param ids the ids of the student exams
* @return the list of student exams with exercises
*/
@Query("""
SELECT DISTINCT se
FROM StudentExam se
LEFT JOIN FETCH se.exercises e
WHERE se.id IN :ids
""")
List<StudentExam> findAllWithEagerExercisesById(List<Long> ids);
}
52 changes: 50 additions & 2 deletions src/main/java/de/tum/in/www1/artemis/service/QuizPoolService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package de.tum.in.www1.artemis.service;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand All @@ -21,14 +24,15 @@
import de.tum.in.www1.artemis.repository.QuizGroupRepository;
import de.tum.in.www1.artemis.repository.QuizPoolRepository;
import de.tum.in.www1.artemis.repository.ShortAnswerMappingRepository;
import de.tum.in.www1.artemis.service.exam.ExamQuizQuestionsGenerator;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

/**
* This service contains the functions to manage QuizPool entity.
*/
@Service
public class QuizPoolService extends QuizService<QuizPool> {
public class QuizPoolService extends QuizService<QuizPool> implements ExamQuizQuestionsGenerator {

private static final String ENTITY_NAME = "quizPool";

Expand Down Expand Up @@ -97,7 +101,7 @@ public QuizPool update(Long examId, QuizPool quizPool) {
* @param examId the id of the exam to be searched
* @return optional quiz pool that belongs to the given exam id
*/
public Optional<QuizPool> findWithQuizQuestionsByExamId(Long examId) {
public Optional<QuizPool> findWithQuizGroupsAndQuestionsByExamId(Long examId) {
Optional<QuizPool> quizPoolOptional = quizPoolRepository.findWithEagerQuizQuestionsByExamId(examId);
if (quizPoolOptional.isPresent()) {
QuizPool quizPool = quizPoolOptional.get();
Expand Down Expand Up @@ -150,4 +154,48 @@ private void removeUnusedQuizGroup(List<Long> existingQuizGroupIds, List<QuizGro
protected QuizPool saveAndFlush(QuizPool quizConfiguration) {
return quizPoolRepository.saveAndFlush(quizConfiguration);
}

@Override
public List<QuizQuestion> generateQuizQuestionsForExam(long examId) {
Optional<QuizPool> quizPoolOptional = findWithQuizGroupsAndQuestionsByExamId(examId);
if (quizPoolOptional.isPresent()) {
QuizPool quizPool = quizPoolOptional.get();
List<QuizGroup> quizGroups = quizPool.getQuizGroups();
List<QuizQuestion> quizQuestions = quizPool.getQuizQuestions();

Map<Long, List<QuizQuestion>> quizGroupQuestionsMap = getQuizQuestionsGroup(quizGroups, quizQuestions);
return generateQuizQuestions(quizGroupQuestionsMap, quizGroups, quizQuestions);
}
else {
return new ArrayList<>();
}
}

private static Map<Long, List<QuizQuestion>> getQuizQuestionsGroup(List<QuizGroup> quizGroups, List<QuizQuestion> quizQuestions) {
Map<Long, List<QuizQuestion>> quizGroupQuestionsMap = new HashMap<>();
for (QuizGroup quizGroup : quizGroups) {
quizGroupQuestionsMap.put(quizGroup.getId(), new ArrayList<>());
}
for (QuizQuestion quizQuestion : quizQuestions) {
if (quizQuestion.getQuizGroup() != null) {
quizGroupQuestionsMap.get(quizQuestion.getQuizGroup().getId()).add(quizQuestion);
}
}
return quizGroupQuestionsMap;
}

private static List<QuizQuestion> generateQuizQuestions(Map<Long, List<QuizQuestion>> quizGroupQuestionsMap, List<QuizGroup> quizGroups, List<QuizQuestion> quizQuestions) {
SecureRandom random = new SecureRandom();
List<QuizQuestion> results = new ArrayList<>();
for (QuizGroup quizGroup : quizGroups) {
List<QuizQuestion> quizGroupQuestions = quizGroupQuestionsMap.get(quizGroup.getId());
results.add(quizGroupQuestions.get(random.nextInt(quizGroupQuestions.size())));
}
for (QuizQuestion quizQuestion : quizQuestions) {
if (quizQuestion.getQuizGroup() == null) {
results.add(quizQuestion);
}
}
return results;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.tum.in.www1.artemis.service.exam;

import java.util.List;

import de.tum.in.www1.artemis.domain.quiz.QuizQuestion;

/**
* Service Interface for generating quiz questions for an exam
*/
public interface ExamQuizQuestionsGenerator {

/**
* Generates quiz questions for an exam
*
* @param examId the id of the exam for which quiz questions should be generated
* @return the list of generated quiz questions
*/
List<QuizQuestion> generateQuizQuestionsForExam(long examId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.cache.CacheManager;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.enumeration.InitializationState;
Expand Down Expand Up @@ -89,13 +90,15 @@ public class StudentExamService {

private final TaskScheduler scheduler;

private final ExamQuizQuestionsGenerator examQuizQuestionsGenerator;

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) {
@Qualifier("taskScheduler") TaskScheduler scheduler, QuizPoolService quizPoolService) {
this.participationService = participationService;
this.studentExamRepository = studentExamRepository;
this.userRepository = userRepository;
Expand All @@ -114,6 +117,7 @@ public StudentExamService(StudentExamRepository studentExamRepository, UserRepos
this.cacheManager = cacheManager;
this.websocketMessagingService = websocketMessagingService;
this.scheduler = scheduler;
this.examQuizQuestionsGenerator = quizPoolService;
}

/**
Expand Down Expand Up @@ -782,6 +786,48 @@ private StudentExam generateIndividualStudentExam(Exam exam, User student) {
// StudentExams are saved in the called method
HashSet<User> userHashSet = new HashSet<>();
userHashSet.add(student);
return studentExamRepository.createRandomStudentExams(exam, userHashSet).get(0);
return studentExamRepository.createRandomStudentExams(exam, userHashSet, examQuizQuestionsGenerator).get(0);
}

/**
* Generates the student exams randomly based on the exam configuration and the exercise groups
* Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded)
*
* @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded
* @return the list of student exams with their corresponding users
*/
@Transactional
public List<StudentExam> generateStudentExams(final Exam exam) {
final var existingStudentExams = studentExamRepository.findByExamId(exam.getId());
// https://jira.spring.io/browse/DATAJPA-1367 deleteInBatch does not work, because it does not cascade the deletion of existing exam sessions, therefore use deleteAll
studentExamRepository.deleteAll(existingStudentExams);

Set<User> users = exam.getRegisteredUsers();

// StudentExams are saved in the called method
return studentExamRepository.createRandomStudentExams(exam, users, examQuizQuestionsGenerator);
}

/**
* Generates the missing student exams randomly based on the exam configuration and the exercise groups.
* The difference between all registered users and the users who already have an individual exam is the set of users for which student exams will be created.
* <p>
* Important: the passed exams needs to include the registered users, exercise groups and exercises (eagerly loaded)
*
* @param exam with eagerly loaded registered users, exerciseGroups and exercises loaded
* @return the list of student exams with their corresponding users
*/
@Transactional
public List<StudentExam> generateMissingStudentExams(Exam exam) {

// Get all users who already have an individual exam
Set<User> usersWithStudentExam = studentExamRepository.findUsersWithStudentExamsForExam(exam.getId());

// Get all students who don't have an exam yet
Set<User> missingUsers = exam.getRegisteredUsers();
missingUsers.removeAll(usersWithStudentExam);

// StudentExams are saved in the called method
return studentExamRepository.createRandomStudentExams(exam, missingUsers, examQuizQuestionsGenerator);
}
}
10 changes: 7 additions & 3 deletions src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,15 @@ public class ExamResource {

private final ExamLiveEventsService examLiveEventsService;

private final StudentExamService studentExamService;

public ExamResource(ProfileService profileService, UserRepository userRepository, CourseRepository courseRepository, ExamService examService,
ExamDeletionService examDeletionService, ExamAccessService examAccessService, InstanceMessageSendService instanceMessageSendService, ExamRepository examRepository,
SubmissionService submissionService, AuthorizationCheckService authCheckService, ExamDateService examDateService,
TutorParticipationRepository tutorParticipationRepository, AssessmentDashboardService assessmentDashboardService, ExamRegistrationService examRegistrationService,
StudentExamRepository studentExamRepository, ExamImportService examImportService, CustomAuditEventRepository auditEventRepository, ChannelService channelService,
ChannelRepository channelRepository, ExerciseRepository exerciseRepository, ExamSessionService examSessionRepository, ExamLiveEventsService examLiveEventsService) {
ChannelRepository channelRepository, ExerciseRepository exerciseRepository, ExamSessionService examSessionRepository, ExamLiveEventsService examLiveEventsService,
StudentExamService studentExamService) {
this.profileService = profileService;
this.userRepository = userRepository;
this.courseRepository = courseRepository;
Expand All @@ -143,6 +146,7 @@ public ExamResource(ProfileService profileService, UserRepository userRepository
this.exerciseRepository = exerciseRepository;
this.examSessionService = examSessionRepository;
this.examLiveEventsService = examLiveEventsService;
this.studentExamService = studentExamService;
}

/**
Expand Down Expand Up @@ -867,7 +871,7 @@ public ResponseEntity<List<StudentExam>> generateStudentExams(@PathVariable Long
// Reset existing student exams & participations in case they already exist
examDeletionService.deleteStudentExamsAndExistingParticipationsForExam(exam.getId());

List<StudentExam> studentExams = studentExamRepository.generateStudentExams(exam);
List<StudentExam> studentExams = studentExamService.generateStudentExams(exam);

// we need to break a cycle for the serialization
breakCyclesForSerialization(studentExams);
Expand Down Expand Up @@ -911,7 +915,7 @@ public ResponseEntity<List<StudentExam>> generateMissingStudentExams(@PathVariab
log.info("REST request to generate missing student exams for exam {}", examId);

final var exam = checkAccessForStudentExamGenerationAndLogAuditEvent(courseId, examId, Constants.GENERATE_MISSING_STUDENT_EXAMS);
List<StudentExam> studentExams = studentExamRepository.generateMissingStudentExams(exam);
List<StudentExam> studentExams = studentExamService.generateMissingStudentExams(exam);

// we need to break a cycle for the serialization
breakCyclesForSerialization(studentExams);
Expand Down
Loading

0 comments on commit 2ea0ab5

Please sign in to comment.