Skip to content

Commit

Permalink
Exam mode: Add quiz pool configuration (#6612)
Browse files Browse the repository at this point in the history
  • Loading branch information
rriyaldhi authored Oct 18, 2023
1 parent cdd3ef0 commit f259364
Show file tree
Hide file tree
Showing 41 changed files with 2,278 additions and 2 deletions.
32 changes: 32 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizGroup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package de.tum.in.www1.artemis.domain.quiz;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.in.www1.artemis.domain.DomainObject;

@Entity
@Table(name = "quiz_group")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class QuizGroup extends DomainObject {

@Column(name = "name")
private String name;

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

@JsonIgnore
public boolean isValid() {
return getName() != null && !getName().isEmpty();
}
}
119 changes: 119 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizPool.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package de.tum.in.www1.artemis.domain.quiz;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;

import de.tum.in.www1.artemis.domain.DomainObject;
import de.tum.in.www1.artemis.domain.exam.Exam;
import de.tum.in.www1.artemis.domain.view.QuizView;

@Entity
@Table(name = "quiz_pool")
@JsonInclude
public class QuizPool extends DomainObject implements QuizConfiguration {

@OneToOne
@JoinColumn(name = "exam_id", referencedColumnName = "id")
private Exam exam;

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "quiz_pool_id", referencedColumnName = "id")
private List<QuizQuestion> quizQuestions;

@Column(name = "max_points")
private int maxPoints;

@Column(name = "randomize_question_order")
@JsonView(QuizView.Before.class)
private Boolean randomizeQuestionOrder = false;

@Transient
private List<QuizGroup> quizGroups;

public QuizPool() {
this.quizGroups = new ArrayList<>();
this.quizQuestions = new ArrayList<>();
}

public void setExam(Exam exam) {
this.exam = exam;
}

public Exam getExam() {
return exam;
}

public int getMaxPoints() {
return maxPoints;
}

public void setMaxPoints(int maxPoints) {
this.maxPoints = maxPoints;
}

public Boolean getRandomizeQuestionOrder() {
return randomizeQuestionOrder;
}

public void setRandomizeQuestionOrder(Boolean randomizeQuestionOrder) {
this.randomizeQuestionOrder = randomizeQuestionOrder;
}

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

@Override
public List<QuizQuestion> getQuizQuestions() {
return this.quizQuestions;
}

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

@JsonProperty(value = "quizGroups", access = JsonProperty.Access.READ_ONLY)
public List<QuizGroup> getQuizGroups() {
return quizGroups;
}

@JsonProperty(value = "quizGroups", access = JsonProperty.Access.WRITE_ONLY)
public void setQuizGroups(List<QuizGroup> quizGroups) {
this.quizGroups = quizGroups;
}

/**
* Check if all quiz groups and questions are valid
*
* @return true if all quiz groups and questions are valid
*/
@JsonIgnore
public boolean isValid() {
for (QuizGroup quizGroup : getQuizGroups()) {
if (!quizGroup.isValid()) {
return false;
}
}
for (QuizQuestion quizQuestion : getQuizQuestions()) {
if (!quizQuestion.isValid()) {
return false;
}
}
return true;
}
}
25 changes: 25 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizQuestion.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public abstract class QuizQuestion extends DomainObject {
@JsonView(QuizView.Before.class)
private Boolean invalid = false;

@Column(name = "quiz_group_id")
@JsonView(QuizView.Before.class)
private Long quizGroupId;

@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
@JoinColumn(unique = true)
private QuizQuestionStatistic quizQuestionStatistic;
Expand All @@ -73,6 +77,9 @@ public abstract class QuizQuestion extends DomainObject {
@JsonIgnore
private QuizExercise exercise;

@Transient
private QuizGroup quizGroup;

public String getTitle() {
return title;
}
Expand Down Expand Up @@ -168,6 +175,24 @@ public void setExercise(QuizExercise quizExercise) {
this.exercise = quizExercise;
}

public Long getQuizGroupId() {
return quizGroupId;
}

public void setQuizGroupId(Long quizGroupId) {
this.quizGroupId = quizGroupId;
}

@JsonProperty(value = "quizGroup", access = JsonProperty.Access.READ_ONLY)
public QuizGroup getQuizGroup() {
return quizGroup;
}

@JsonProperty(value = "quizGroup", access = JsonProperty.Access.WRITE_ONLY)
public void setQuizGroup(QuizGroup quizGroup) {
this.quizGroup = quizGroup;
}

/**
* Calculate the score for the given answer
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package de.tum.in.www1.artemis.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

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

/**
* Spring Data JPA repository for the QuizGroup entity.
*/
@SuppressWarnings("unused")
@Repository
public interface QuizGroupRepository extends JpaRepository<QuizGroup, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package de.tum.in.www1.artemis.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

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

/**
* Spring Data JPA repository for the QuizPool entity.
*/
@SuppressWarnings("unused")
@Repository
public interface QuizPoolRepository extends JpaRepository<QuizPool, Long> {

@Query("""
SELECT qe
FROM QuizPool qe
JOIN qe.exam e
LEFT JOIN FETCH qe.quizQuestions qeq
LEFT JOIN FETCH qeq.quizQuestionStatistic
WHERE e.id = :examId
""")
Optional<QuizPool> findWithEagerQuizQuestionsByExamId(Long examId);
}
139 changes: 139 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/service/QuizPoolService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package de.tum.in.www1.artemis.service;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import de.tum.in.www1.artemis.domain.exam.Exam;
import de.tum.in.www1.artemis.domain.quiz.QuizGroup;
import de.tum.in.www1.artemis.domain.quiz.QuizPool;
import de.tum.in.www1.artemis.domain.quiz.QuizQuestion;
import de.tum.in.www1.artemis.repository.DragAndDropMappingRepository;
import de.tum.in.www1.artemis.repository.ExamRepository;
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.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> {

private static final String ENTITY_NAME = "quizPool";

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

private final QuizPoolRepository quizPoolRepository;

private final QuizGroupRepository quizGroupRepository;

private final ExamRepository examRepository;

public QuizPoolService(DragAndDropMappingRepository dragAndDropMappingRepository, ShortAnswerMappingRepository shortAnswerMappingRepository,
QuizPoolRepository quizPoolRepository, QuizGroupRepository quizGroupRepository, ExamRepository examRepository) {
super(dragAndDropMappingRepository, shortAnswerMappingRepository);
this.quizPoolRepository = quizPoolRepository;
this.quizGroupRepository = quizGroupRepository;
this.examRepository = examRepository;
}

/**
* Check if the given exam id is valid, then update quiz pool that belongs to the given exam id
*
* @param examId the id of the exam to be checked
* @param quizPool the quiz pool to be updated
* @return updated quiz pool
*/
public QuizPool update(Long examId, QuizPool quizPool) {
Exam exam = examRepository.findByIdElseThrow(examId);

quizPool.setExam(exam);

if (quizPool.getQuizQuestions() == null || !quizPool.isValid()) {
throw new BadRequestAlertException("The quiz pool is invalid", ENTITY_NAME, "invalidQuiz");
}

List<QuizGroup> savedQuizGroups = quizGroupRepository.saveAllAndFlush(quizPool.getQuizGroups());
quizPoolRepository.findWithEagerQuizQuestionsByExamId(examId).ifPresent(existingQuizPool -> {
List<Long> existingQuizGroupIds = existingQuizPool.getQuizQuestions().stream().map(QuizQuestion::getQuizGroupId).filter(Objects::nonNull).toList();
removeUnusedQuizGroup(existingQuizGroupIds, savedQuizGroups);
});

Map<String, Long> quizGroupNameIdMap = savedQuizGroups.stream().collect(Collectors.toMap(QuizGroup::getName, QuizGroup::getId));
for (QuizQuestion quizQuestion : quizPool.getQuizQuestions()) {
if (quizQuestion.getQuizGroup() != null) {
quizQuestion.setQuizGroupId(quizGroupNameIdMap.get(quizQuestion.getQuizGroup().getName()));
}
else {
quizQuestion.setQuizGroupId(null);
}
}
quizPool.reconnectJSONIgnoreAttributes();

log.debug("Save quiz pool to database: {}", quizPool);
super.save(quizPool);

QuizPool savedQuizPool = quizPoolRepository.findWithEagerQuizQuestionsByExamId(examId).orElseThrow(() -> new EntityNotFoundException(ENTITY_NAME, "examId=" + examId));
savedQuizPool.setQuizGroups(savedQuizGroups);
reassignQuizQuestion(savedQuizPool, savedQuizGroups);

return savedQuizPool;
}

/**
* Find a quiz pool (if exists) that belongs to the given exam id
*
* @param examId the id of the exam to be searched
* @return quiz pool that belongs to the given exam id
*/
public QuizPool findByExamId(Long examId) {
QuizPool quizPool = quizPoolRepository.findWithEagerQuizQuestionsByExamId(examId).orElseThrow(() -> new EntityNotFoundException(ENTITY_NAME, "examId=" + examId));
List<Long> quizGroupIds = quizPool.getQuizQuestions().stream().map(QuizQuestion::getQuizGroupId).filter(Objects::nonNull).toList();
List<QuizGroup> quizGroups = quizGroupRepository.findAllById(quizGroupIds);
quizPool.setQuizGroups(quizGroups);
reassignQuizQuestion(quizPool, quizGroups);
return quizPool;
}

/**
* Reassign the connection between quiz question, quiz pool and quiz group
*
* @param quizPool the quiz pool to be reset
* @param quizGroups the list of quiz group to be reset
*/
private void reassignQuizQuestion(QuizPool quizPool, List<QuizGroup> quizGroups) {
Map<Long, QuizGroup> idQuizGroupMap = quizGroups.stream().collect(Collectors.toMap(QuizGroup::getId, Function.identity()));
for (QuizQuestion quizQuestion : quizPool.getQuizQuestions()) {
if (quizQuestion.getQuizGroupId() != null) {
quizQuestion.setQuizGroup(idQuizGroupMap.get(quizQuestion.getQuizGroupId()));
}
}
}

/**
* Remove existing groups that do not exist anymore in the updated quiz pool
*
* @param existingQuizGroupIds the list of existing quiz group id of the quiz pool
* @param usedQuizGroups the list of quiz group that are still exists in the updated quiz pool
*/
private void removeUnusedQuizGroup(List<Long> existingQuizGroupIds, List<QuizGroup> usedQuizGroups) {
Set<Long> usedQuizGroupIds = usedQuizGroups.stream().map(QuizGroup::getId).collect(Collectors.toSet());
Set<Long> ids = existingQuizGroupIds.stream().filter(id -> !usedQuizGroupIds.contains(id)).collect(Collectors.toSet());
quizGroupRepository.deleteAllById(ids);
}

@Override
protected QuizPool saveAndFlush(QuizPool quizConfiguration) {
return quizPoolRepository.saveAndFlush(quizConfiguration);
}
}
Loading

0 comments on commit f259364

Please sign in to comment.