diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/controller/QuestionnaireEvaluationController.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/controller/QuestionnaireEvaluationController.java new file mode 100644 index 0000000000000000000000000000000000000000..af9ae3971aade5f75d62481c6808ed1139dc2e07 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/controller/QuestionnaireEvaluationController.java @@ -0,0 +1,58 @@ +package cz.muni.ics.kypo.training.adaptive.controller; + +import cz.muni.ics.kypo.training.adaptive.dto.run.QuestionnairePhaseAnswersDTO; +import cz.muni.ics.kypo.training.adaptive.facade.QuestionnaireEvaluationFacade; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@RestController +@RequestMapping(value = "/training-runs", produces = MediaType.APPLICATION_JSON_VALUE) +@CrossOrigin(origins = "*", allowCredentials = "true", allowedHeaders = "*", + methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.DELETE, RequestMethod.PUT}) +@Api(value = "/training-runs", + tags = "Training runs", + consumes = MediaType.APPLICATION_JSON_VALUE, + authorizations = @Authorization(value = "bearerAuth")) +public class QuestionnaireEvaluationController { + + private final QuestionnaireEvaluationFacade questionnaireEvaluationFacade; + + @Autowired + public QuestionnaireEvaluationController(QuestionnaireEvaluationFacade questionnaireEvaluationFacade) { + this.questionnaireEvaluationFacade = questionnaireEvaluationFacade; + } + + @ApiOperation(httpMethod = "POST", + value = "Evaluate answers to a questionnaire phase", + nickname = "evaluateAnswersToQuestionnaire", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Answers evaluated"), + @ApiResponse(code = 500, message = "Unexpected application error") + }) + @PutMapping(value = "/{runId}/questionnaire-evaluation", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity<Void> evaluateAnswersToQuestionnaire(@ApiParam(value = "Training run ID", required = true) + @PathVariable("runId") Long runId, + @ApiParam(value = "Responses to questionnaire", required = true) + @Valid @RequestBody QuestionnairePhaseAnswersDTO questionnairePhaseAnswersDTO) { + questionnaireEvaluationFacade.evaluateAnswersToQuestionnaire(runId, questionnairePhaseAnswersDTO); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionAnswer.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionAnswer.java new file mode 100644 index 0000000000000000000000000000000000000000..211b904f7e0768cc2cce281416faaa17d605de1d --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionAnswer.java @@ -0,0 +1,32 @@ +package cz.muni.ics.kypo.training.adaptive.domain; + +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.io.Serializable; + +@Entity +@Table(name = "question_answer") +public class QuestionAnswer implements Serializable { + + @EmbeddedId + private QuestionAnswerId questionAnswerId; + + private String answer; + + public QuestionAnswerId getQuestionAnswerId() { + return questionAnswerId; + } + + public void setQuestionAnswerId(QuestionAnswerId questionAnswerId) { + this.questionAnswerId = questionAnswerId; + } + + public String getAnswer() { + return answer; + } + + public void setAnswer(String answer) { + this.answer = answer; + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionAnswerId.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionAnswerId.java new file mode 100644 index 0000000000000000000000000000000000000000..d59dff5ccdf7b39773de264a7dd56601a3d582d7 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionAnswerId.java @@ -0,0 +1,49 @@ +package cz.muni.ics.kypo.training.adaptive.domain; + +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public class QuestionAnswerId implements Serializable { + private Question question; + private Long trainingRunId; + + public QuestionAnswerId() { + } + + public QuestionAnswerId(Question question, Long trainingRunId) { + this.question = question; + this.trainingRunId = trainingRunId; + } + + public Question getQuestion() { + return question; + } + + public void setQuestion(Question question) { + this.question = question; + } + + public Long getTrainingRunId() { + return trainingRunId; + } + + public void setTrainingRunId(Long trainingRunId) { + this.trainingRunId = trainingRunId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QuestionAnswerId that = (QuestionAnswerId) o; + return Objects.equals(question, that.question) && + Objects.equals(trainingRunId, that.trainingRunId); + } + + @Override + public int hashCode() { + return Objects.hash(question, trainingRunId); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionPhaseResult.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionPhaseResult.java new file mode 100644 index 0000000000000000000000000000000000000000..cfc2bf0de2ab73fc2c22897f37d35d228092bcb3 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/QuestionPhaseResult.java @@ -0,0 +1,61 @@ +package cz.muni.ics.kypo.training.adaptive.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import java.io.Serializable; + +@Entity +@Table(name = "question_phase_result") +public class QuestionPhaseResult implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "questionPhaseResultGenerator") + @SequenceGenerator(name = "questionPhaseResultGenerator", sequenceName = "question_phase_result_seq") + @Column(name = "question_phase_result_id", nullable = false, unique = true) + private Long id; + + private Long trainingRunId; + private int achievedResult; + + @ManyToOne(fetch = FetchType.LAZY) + private QuestionPhaseRelation questionPhaseRelation; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getTrainingRunId() { + return trainingRunId; + } + + public void setTrainingRunId(Long trainingRunId) { + this.trainingRunId = trainingRunId; + } + + public int getAchievedResult() { + return achievedResult; + } + + public void setAchievedResult(int achievedResult) { + this.achievedResult = achievedResult; + } + + public QuestionPhaseRelation getQuestionPhaseRelation() { + return questionPhaseRelation; + } + + public void setQuestionPhaseRelation(QuestionPhaseRelation questionPhaseRelation) { + this.questionPhaseRelation = questionPhaseRelation; + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/run/QuestionAnswerDTO.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/run/QuestionAnswerDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..772e2fef5e9ca86d9498f8ba97be45838f8f84f6 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/run/QuestionAnswerDTO.java @@ -0,0 +1,31 @@ +package cz.muni.ics.kypo.training.adaptive.dto.run; + +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.constraints.NotNull; + +public class QuestionAnswerDTO { + + @ApiModelProperty(value = "ID of answered question", example = "1") + @NotNull(message = "ID of the answered question must not be null") + private Long questionId; + + @ApiModelProperty(value = "Answer to the question", example = "An answer") + private String answer; + + public Long getQuestionId() { + return questionId; + } + + public void setQuestionId(Long questionId) { + this.questionId = questionId; + } + + public String getAnswer() { + return answer; + } + + public void setAnswer(String answer) { + this.answer = answer; + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/run/QuestionnairePhaseAnswersDTO.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/run/QuestionnairePhaseAnswersDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..9a4eecd60731d4999c60ec5c499a7aaeb7ffc64e --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/run/QuestionnairePhaseAnswersDTO.java @@ -0,0 +1,21 @@ +package cz.muni.ics.kypo.training.adaptive.dto.run; + +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import java.util.List; + +public class QuestionnairePhaseAnswersDTO { + + @Valid + @ApiModelProperty(value = "Answers to the questionnaire provided by user", required = true) + private List<QuestionAnswerDTO> answers; + + public List<QuestionAnswerDTO> getAnswers() { + return answers; + } + + public void setAnswers(List<QuestionAnswerDTO> answers) { + this.answers = answers; + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/QuestionnaireEvaluationFacade.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/QuestionnaireEvaluationFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..fd657740cb375d2aaef4ec50eb08794d848af01a --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/QuestionnaireEvaluationFacade.java @@ -0,0 +1,25 @@ +package cz.muni.ics.kypo.training.adaptive.facade; + +import cz.muni.ics.kypo.training.adaptive.domain.QuestionAnswer; +import cz.muni.ics.kypo.training.adaptive.dto.run.QuestionnairePhaseAnswersDTO; +import cz.muni.ics.kypo.training.adaptive.service.QuestionnaireEvaluationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class QuestionnaireEvaluationFacade { + + private final QuestionnaireEvaluationService questionnaireEvaluationService; + + @Autowired + public QuestionnaireEvaluationFacade(QuestionnaireEvaluationService questionnaireEvaluationService) { + this.questionnaireEvaluationService = questionnaireEvaluationService; + } + + public void evaluateAnswersToQuestionnaire(Long runId, QuestionnairePhaseAnswersDTO questionnairePhaseAnswersDTO) { + List<QuestionAnswer> savedAnswers = questionnaireEvaluationService.saveAnswersToQuestionnaire(runId, questionnairePhaseAnswersDTO); + questionnaireEvaluationService.evaluateAnswersToQuestionnaire(runId, savedAnswers); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/QuestionAnswerRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/QuestionAnswerRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..2c5e54e8ab08423f376c53b068f9768672a78e02 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/QuestionAnswerRepository.java @@ -0,0 +1,10 @@ +package cz.muni.ics.kypo.training.adaptive.repository; + +import cz.muni.ics.kypo.training.adaptive.domain.QuestionAnswer; +import cz.muni.ics.kypo.training.adaptive.domain.QuestionAnswerId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QuestionAnswerRepository extends JpaRepository<QuestionAnswer, QuestionAnswerId> { +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/QuestionPhaseResultRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/QuestionPhaseResultRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..86c9945d90e33876ca59d3496d20309ee5264a90 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/QuestionPhaseResultRepository.java @@ -0,0 +1,9 @@ +package cz.muni.ics.kypo.training.adaptive.repository; + +import cz.muni.ics.kypo.training.adaptive.domain.QuestionPhaseResult; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QuestionPhaseResultRepository extends JpaRepository<QuestionPhaseResult, Long> { +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/QuestionPhaseRelationRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/QuestionPhaseRelationRepository.java index e186d22fce5a3a1b569e9f3abe4eb304354a114e..25dd89460bfb525790974d306b6e32ecae5c7970 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/QuestionPhaseRelationRepository.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/QuestionPhaseRelationRepository.java @@ -2,8 +2,16 @@ package cz.muni.ics.kypo.training.adaptive.repository.phases; import cz.muni.ics.kypo.training.adaptive.domain.phase.questions.QuestionPhaseRelation; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Set; + @Repository public interface QuestionPhaseRelationRepository extends JpaRepository<QuestionPhaseRelation, Long> { + + @Query("SELECT r FROM QuestionPhaseRelation r INNER JOIN r.questions q WHERE q.id IN :questionIdList") + List<QuestionPhaseRelation> findAllByQuestionIdList(@Param("questionIdList") Set<Long> questionIdList); } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnaireEvaluationService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnaireEvaluationService.java new file mode 100644 index 0000000000000000000000000000000000000000..c1d70ef79527c3377621353cc11f78aded7e8ddc --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnaireEvaluationService.java @@ -0,0 +1,121 @@ +package cz.muni.ics.kypo.training.adaptive.service; + +import cz.muni.ics.kypo.training.adaptive.domain.Question; +import cz.muni.ics.kypo.training.adaptive.domain.QuestionAnswer; +import cz.muni.ics.kypo.training.adaptive.domain.QuestionAnswerId; +import cz.muni.ics.kypo.training.adaptive.domain.QuestionChoice; +import cz.muni.ics.kypo.training.adaptive.domain.QuestionPhaseRelation; +import cz.muni.ics.kypo.training.adaptive.domain.QuestionPhaseResult; +import cz.muni.ics.kypo.training.adaptive.dto.run.QuestionAnswerDTO; +import cz.muni.ics.kypo.training.adaptive.dto.run.QuestionnairePhaseAnswersDTO; +import cz.muni.ics.kypo.training.adaptive.repository.QuestionAnswerRepository; +import cz.muni.ics.kypo.training.adaptive.repository.QuestionPhaseRelationRepository; +import cz.muni.ics.kypo.training.adaptive.repository.QuestionPhaseResultRepository; +import cz.muni.ics.kypo.training.adaptive.repository.QuestionRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Transactional +public class QuestionnaireEvaluationService { + + private static final Logger LOG = LoggerFactory.getLogger(QuestionnaireEvaluationService.class); + + private final QuestionRepository questionRepository; + private final QuestionAnswerRepository questionAnswerRepository; + private final QuestionPhaseRelationRepository questionPhaseRelationRepository; + private final QuestionPhaseResultRepository questionPhaseResultRepository; + + @Autowired + public QuestionnaireEvaluationService(QuestionRepository questionRepository, QuestionAnswerRepository questionAnswerRepository, + QuestionPhaseRelationRepository questionPhaseRelationRepository, QuestionPhaseResultRepository questionPhaseResultRepository) { + this.questionRepository = questionRepository; + this.questionAnswerRepository = questionAnswerRepository; + this.questionPhaseRelationRepository = questionPhaseRelationRepository; + this.questionPhaseResultRepository = questionPhaseResultRepository; + } + + public List<QuestionAnswer> saveAnswersToQuestionnaire(Long runId, QuestionnairePhaseAnswersDTO questionnairePhaseAnswersDTO) { + List<QuestionAnswer> result = new ArrayList<>(); + for (QuestionAnswerDTO answer : questionnairePhaseAnswersDTO.getAnswers()) { + Question question = questionRepository.findById(answer.getQuestionId()) + .orElseThrow(() -> new RuntimeException("Question was not found")); + // TODO throw proper exception once kypo2-training is migrated + + QuestionAnswer questionAnswer = new QuestionAnswer(); + questionAnswer.setQuestionAnswerId(new QuestionAnswerId(question, runId)); + questionAnswer.setAnswer(answer.getAnswer()); + + questionAnswerRepository.save(questionAnswer); + result.add(questionAnswer); + } + + return result; + } + + public void evaluateAnswersToQuestionnaire(Long runId, List<QuestionAnswer> answers) { + if (CollectionUtils.isEmpty(answers)) { + return; + } + + Set<Long> questionIdList = answers.stream() + .map(QuestionAnswer::getQuestionAnswerId) + .map(QuestionAnswerId::getQuestion) + .map(Question::getId) + .collect(Collectors.toSet()); + + List<QuestionPhaseRelation> questionPhaseRelations = questionPhaseRelationRepository.findAllByQuestionIdList(questionIdList); + for (QuestionPhaseRelation questionPhaseRelation : questionPhaseRelations) { + int numberOfCorrectAnswers = 0; + if (CollectionUtils.isEmpty(questionPhaseRelation.getQuestions())) { + LOG.warn("No questions found for question phase relation {}", questionPhaseRelation.getId()); + continue; + } + + for (Question question : questionPhaseRelation.getQuestions()) { + Optional<QuestionAnswer> correspondingAnswer = findCorrespondingAnswer(question.getId(), answers); + if (correspondingAnswer.isPresent()) { + String answer = correspondingAnswer.get().getAnswer(); + Optional<QuestionChoice> correspondingQuestionChoice = findCorrespondingQuestionChoice(answer, question.getChoices()); + if (correspondingQuestionChoice.isPresent() && correspondingQuestionChoice.get().isCorrect()) { + numberOfCorrectAnswers++; + } + } else { + LOG.debug("No answer found for question {}. It is assumed as a wrong answer", question.getId()); + } + } + + int achievedResult = numberOfCorrectAnswers * 100 / questionPhaseRelation.getQuestions().size(); + + QuestionPhaseResult questionPhaseResult = new QuestionPhaseResult(); + questionPhaseResult.setAchievedResult(achievedResult); + questionPhaseResult.setQuestionPhaseRelation(questionPhaseRelation); + questionPhaseResult.setTrainingRunId(runId); + + questionPhaseResultRepository.save(questionPhaseResult); + } + } + + private Optional<QuestionAnswer> findCorrespondingAnswer(Long questionId, List<QuestionAnswer> answers) { + return answers.stream() + .filter(a -> Objects.equals(questionId, a.getQuestionAnswerId().getQuestion().getId())) + .findFirst(); + } + + private Optional<QuestionChoice> findCorrespondingQuestionChoice(String answer, List<QuestionChoice> questionChoices) { + return questionChoices.stream() + .filter(q -> Objects.equals(answer, q.getText())) + .findFirst(); + } +}