Skip to content
Snippets Groups Projects
Commit 95282823 authored by Pavel Šeda's avatar Pavel Šeda
Browse files

Merge branch 'ft-questionnaires-evaluation' into 'master'

First iteration of questionnaire answers evaluation

See merge request muni-kypo/kypolab/kypo-adaptive-training!16
parents 0822b32c d83edb14
No related branches found
No related tags found
1 merge request!16First iteration of questionnaire answers evaluation
Pipeline #73757 failed with stages
in 1 minute and 21 seconds
Showing
with 425 additions and 0 deletions
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();
}
}
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;
}
}
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);
}
}
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;
}
}
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;
}
}
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;
}
}
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);
}
}
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> {
}
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> {
}
......@@ -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);
}
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();
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment