diff --git a/VERSION.txt b/VERSION.txt index 8ce7a0b24905af13a0e7084bbd3e29fb675274a1..03f4ac328305d953e64f6020bf5ad2a871c188f7 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1,3 +1,6 @@ +2.2.8 Add check for training definition being in use when trying to add, remove and update tasks. +2.2.7 Add more descriptive message when training phase contains no tasks. Add answer required field to Adaptive Question. +2.2.6 Implement the ANSWER tag replacing in task solution. 2.2.5 Fix ATD update not setting createdAt. 2.2.4 Fix ATD simulator not accepting passkey content field. 2.2.3 Add allocation id to training run. diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phase/questions/Question.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phase/questions/Question.java index fb0848cacd311d14e68b5eda171c784bb68fed32..2538a6ccc53fda15faa6894cb290f798b80b90cc 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phase/questions/Question.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phase/questions/Question.java @@ -40,6 +40,9 @@ public class Question implements Serializable { ) private Set<QuestionPhaseRelation> questionPhaseRelations = new HashSet<>(); + @Column(name = "answer_required") + private boolean answerRequired; + public Long getId() { return id; } @@ -100,6 +103,14 @@ public class Question implements Serializable { this.questionPhaseRelations.add(questionPhaseRelation); } + public boolean isAnswerRequired() { + return answerRequired; + } + + public void setAnswerRequired(boolean answerRequired) { + this.answerRequired = answerRequired; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -107,12 +118,13 @@ public class Question implements Serializable { Question question = (Question) o; return getOrder() == question.getOrder() && getQuestionType() == question.getQuestionType() && - Objects.equals(getText(), question.getText()); + Objects.equals(getText(), question.getText()) && + Objects.equals(isAnswerRequired(), question.isAnswerRequired()); } @Override public int hashCode() { - return Objects.hash(getQuestionType(), getText(), getOrder()); + return Objects.hash(getQuestionType(), getText(), getOrder(), isAnswerRequired()); } @Override @@ -122,6 +134,7 @@ public class Question implements Serializable { ", questionType=" + this.getQuestionType() + ", text='" + this.getText() + '\'' + ", order=" + this.getOrder() + + ", answerRequired=" + this.answerRequired + '}'; } } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/AbstractQuestionDTO.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/AbstractQuestionDTO.java index e9075d267e898358c34ab5a533f1bb00807f9a55..bb770d15ec50f86de4a16d3150046dd0481c60b5 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/AbstractQuestionDTO.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/AbstractQuestionDTO.java @@ -29,6 +29,9 @@ public abstract class AbstractQuestionDTO { @Valid @ApiModelProperty(value = "Choices that are distributed with the question", required = true) private List<QuestionChoiceDTO> choices = new ArrayList<>(); + @ApiModelProperty(value = "Sign if the question must be answered by the participant or not.", example = "true") + @NotNull(message = "{question.answerRequired.NotNull.message}") + private boolean answerRequired; public int getOrder() { return order; @@ -62,12 +65,21 @@ public abstract class AbstractQuestionDTO { this.choices = choices; } + public boolean isAnswerRequired() { + return answerRequired; + } + + public void setAnswerRequired(boolean answerRequired) { + this.answerRequired = answerRequired; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AbstractQuestionDTO that = (AbstractQuestionDTO) o; return order == that.order && + answerRequired == that.answerRequired && Objects.equals(text, that.text) && questionType == that.questionType && Objects.equals(choices, that.choices); @@ -75,7 +87,7 @@ public abstract class AbstractQuestionDTO { @Override public int hashCode() { - return Objects.hash(order, text, questionType, choices); + return Objects.hash(order, text, questionType, choices, answerRequired); } @Override diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/view/QuestionViewDTO.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/view/QuestionViewDTO.java index ebdcb24dc04f7320cdb2c67f1dc07746f4bc996f..7f62bbc20c1f20f3f4a47dab3fdd5f60af77f4a4 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/view/QuestionViewDTO.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/questionnaire/view/QuestionViewDTO.java @@ -3,6 +3,7 @@ package cz.muni.ics.kypo.training.adaptive.dto.questionnaire.view; import cz.muni.ics.kypo.training.adaptive.enums.QuestionType; import io.swagger.annotations.ApiModelProperty; +import javax.validation.constraints.NotNull; import java.util.List; import java.util.Objects; @@ -18,6 +19,8 @@ public class QuestionViewDTO { private QuestionType questionType; @ApiModelProperty(value = "Choices that are distributed with the question", required = true) private List<QuestionChoiceViewDTO> choices; + @ApiModelProperty(value = "Sign if the question must be answered by the participant or not.", example = "true") + private boolean answerRequired; public Long getId() { return id; @@ -59,6 +62,14 @@ public class QuestionViewDTO { this.choices = choices; } + public boolean isAnswerRequired() { + return answerRequired; + } + + public void setAnswerRequired(boolean answerRequired) { + this.answerRequired = answerRequired; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -67,12 +78,13 @@ public class QuestionViewDTO { return getOrder() == that.getOrder() && getId().equals(that.getId()) && getText().equals(that.getText()) && - getQuestionType() == that.getQuestionType(); + getQuestionType() == that.getQuestionType() && + isAnswerRequired() == that.isAnswerRequired(); } @Override public int hashCode() { - return Objects.hash(getId(), getOrder(), getText()); + return Objects.hash(getId(), getOrder(), getText(), isAnswerRequired()); } @Override @@ -82,6 +94,7 @@ public class QuestionViewDTO { ", order=" + order + ", text='" + text + '\'' + ", questionType=" + questionType + + ", answerRequired=" + answerRequired + '}'; } } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TaskFacade.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TaskFacade.java index f9d0b242f0716d73873bb506633dc95463f827e6..39e438f0b0591efdab99c4647a9e3c83f8404301 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TaskFacade.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TaskFacade.java @@ -53,6 +53,7 @@ public class TaskFacade { @TransactionalWO public TaskDTO createDefaultTask(Long phaseId) { AbstractPhase trainingPhase = phaseService.getPhase(phaseId); + trainingDefinitionService.checkIfCanBeUpdated(trainingPhase.getTrainingDefinition()); if (!(trainingPhase instanceof TrainingPhase)) { throw new EntityConflictException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, "The specified phase isn't training phase.")); } @@ -66,6 +67,7 @@ public class TaskFacade { @TransactionalWO public TaskDTO createTask(Long phaseId, TaskCopyDTO taskCopyDTO) { AbstractPhase trainingPhase = this.phaseService.getPhase(phaseId); + trainingDefinitionService.checkIfCanBeUpdated(trainingPhase.getTrainingDefinition()); if (!(trainingPhase instanceof TrainingPhase)) { throw new EntityConflictException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, "The specified phase isn't training phase.")); } @@ -99,6 +101,8 @@ public class TaskFacade { "or @securityService.isDesignerOfGivenTask(#taskId)") @TransactionalWO public TaskDTO updateTask(Long taskId, TaskUpdateDTO taskUpdateDto) { + TrainingDefinition trainingDefinition = this.taskService.getTask(taskId).getTrainingPhase().getTrainingDefinition(); + trainingDefinitionService.checkIfCanBeUpdated(trainingDefinition); Task updatedTask = this.taskService.updateTask(taskId, this.taskMapper.mapToEntity(taskUpdateDto)); trainingDefinitionService.auditAndSave(updatedTask.getTrainingPhase().getTrainingDefinition()); return this.taskMapper.mapToTaskDTO(updatedTask); @@ -115,6 +119,7 @@ public class TaskFacade { public void removeTask(Long taskId) { Task taskToRemove = this.taskService.getTask(taskId); TrainingDefinition relatedTrainingDefinition = taskToRemove.getTrainingPhase().getTrainingDefinition(); + trainingDefinitionService.checkIfCanBeUpdated(relatedTrainingDefinition); this.taskService.removeTask(taskToRemove); trainingDefinitionService.auditAndSave(relatedTrainingDefinition); } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TrainingRunFacade.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TrainingRunFacade.java index ef74eb3b51845baeda827034943107762c2e10e6..ebbfd1d0a1ee951dfe1f22e6f386aca04ee2990d 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TrainingRunFacade.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/facade/TrainingRunFacade.java @@ -521,8 +521,12 @@ public class TrainingRunFacade { TrainingPhasePreviewDTO trainingPhasePreviewDTO = phaseMapper.mapToTrainingPhasePreviewDTO(trainingPhase, visitedTask); boolean isSolutionTaken = trainingRun.getSolutionInfoList().stream() .anyMatch(solutionInfo -> trainingPhase.getId().equals(solutionInfo.getTrainingPhaseId())); + String solutionWithAnswers = trainingPhasePreviewDTO.getTask().getSolution(); if (!isSolutionTaken) { trainingPhasePreviewDTO.getTask().setSolution(null); + } else if (solutionWithAnswers.contains("${ANSWER}")) { + solutionWithAnswers = solutionWithAnswers.replaceAll("\\$\\{ANSWER\\}", trainingRunService.getSolution(trainingRun.getId())); + trainingPhasePreviewDTO.getTask().setSolution(solutionWithAnswers); } return trainingPhasePreviewDTO; } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java index c47ae1efe42567f29e77d7bdc793db67a00bef6b..797c046334b271d10b7c643386ff4d048fd1a067 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java @@ -547,6 +547,13 @@ public class TrainingRunService { throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstance.getId().getClass(), trainingInstance.getId(), "At first organizer must allocate sandboxes for training instance.")); } + List<AbstractPhase> phases = abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(trainingInstance.getTrainingDefinition().getId()); + for (var phase: phases) { + if (phase instanceof TrainingPhase && ((TrainingPhase) phase).getTasks().isEmpty()) { + throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstance.getId().getClass(), trainingInstance.getId(), + "Training phase " + phase.getOrder() + " contains no tasks.")); + } + } return trainingInstance; } @@ -745,7 +752,11 @@ public class TrainingRunService { trainingRunRepository.save(trainingRun); auditEventsService.auditSolutionDisplayedAction(trainingRun); } - return trainingRun.getCurrentTask().getSolution(); + String solution = trainingRun.getCurrentTask().getSolution(); + if (solution.contains("${ANSWER}")) { + solution = solution.replaceAll("\\$\\{ANSWER\\}", trainingRun.getCurrentTask().getAnswer()); + } + return solution; } else { throw new BadRequestException("Current phase is not training phase and does not have solution."); } diff --git a/src/main/resources/db/migration/V13__db_question_answer_required.sql b/src/main/resources/db/migration/V13__db_question_answer_required.sql new file mode 100644 index 0000000000000000000000000000000000000000..4785c16394158e0953a141b578adc44f2b16e954 --- /dev/null +++ b/src/main/resources/db/migration/V13__db_question_answer_required.sql @@ -0,0 +1 @@ +alter table question add column answer_required boolean;