Skip to content
Snippets Groups Projects
Commit d83edb14 authored by Jan Tymel's avatar Jan Tymel
Browse files

First iteration of questionnaire answers evaluation

parent 6a6929ae
No related branches found
No related tags found
1 merge request!16First iteration of questionnaire answers evaluation
Pipeline #73722 passed with stages
in 1 minute and 10 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> {
}
......@@ -2,8 +2,16 @@ package cz.muni.ics.kypo.training.adaptive.repository;
import cz.muni.ics.kypo.training.adaptive.domain.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.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> {
}
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