Index: lams_central/src/java/org/lamsfoundation/lams/web/qb/ImsQtiController.java =================================================================== diff -u -r71ecf7fae19f2c5bb42f613c3a72b062b145767e -rc6f5e5e1aeb31463657f9fe739fc889d2d7e196d --- lams_central/src/java/org/lamsfoundation/lams/web/qb/ImsQtiController.java (.../ImsQtiController.java) (revision 71ecf7fae19f2c5bb42f613c3a72b062b145767e) +++ lams_central/src/java/org/lamsfoundation/lams/web/qb/ImsQtiController.java (.../ImsQtiController.java) (revision c6f5e5e1aeb31463657f9fe739fc889d2d7e196d) @@ -83,10 +83,11 @@ for (Question question : questions) { String uuid = question.getQbUUID(); + QbQuestion qbQuestion = null; // try to match the question to an existing QB question in DB if (uuid != null) { - QbQuestion qbQuestion = qbService.getQuestionByUUID(UUID.fromString(uuid)); + qbQuestion = qbService.getQuestionByUUID(UUID.fromString(uuid)); if (qbQuestion != null) { // found an existing question with same UUID // now check if it is in the collection already @@ -99,7 +100,7 @@ if (collectionUUIDs.contains(uuid)) { if (log.isDebugEnabled()) { - log.debug("Skipping an existing question. Name: " + qbQuestion.getName() + ", uid: " + log.debug("Found existing question. Name: " + qbQuestion.getName() + ", uid: " + qbQuestion.getUid()); } } else { @@ -111,231 +112,235 @@ + ", uid: " + qbQuestion.getUid()); } } - continue; } } - QbQuestion qbQuestion = new QbQuestion(); - qbQuestion.setUuid(uuid); - qbQuestion.setName(question.getTitle()); - qbQuestion.setContentFolderId( - StringUtils.isBlank(contentFolderID) ? FileUtil.generateUniqueContentFolderID() : contentFolderID); - qbQuestion.setDescription(QuestionParser.processHTMLField(question.getText(), false, - qbQuestion.getContentFolderId(), question.getResourcesFolderPath())); - qbQuestion.setFeedback(QuestionParser.processHTMLField(question.getFeedback(), false, - qbQuestion.getContentFolderId(), question.getResourcesFolderPath())); - qbQuestion.setPenaltyFactor(0); - int questionId = qbService.generateNextQuestionId(); - qbQuestion.setQuestionId(questionId); - qbQuestion.setVersion(1); + if (qbQuestion == null) { - Integer questionMark = question.getScore(); - boolean isMultipleChoice = Question.QUESTION_TYPE_MULTIPLE_CHOICE.equals(question.getType()); - boolean isMarkHedgingType = Question.QUESTION_TYPE_MARK_HEDGING.equals(question.getType()); - boolean isVsaType = Question.QUESTION_TYPE_FILL_IN_BLANK.contentEquals(question.getType()); + qbQuestion = new QbQuestion(); + qbQuestion.setUuid(uuid); + qbQuestion.setName(question.getTitle()); + qbQuestion.setContentFolderId( + StringUtils.isBlank(contentFolderID) ? FileUtil.generateUniqueContentFolderID() + : contentFolderID); + qbQuestion.setDescription(QuestionParser.processHTMLField(question.getText(), false, + qbQuestion.getContentFolderId(), question.getResourcesFolderPath())); + qbQuestion.setFeedback(QuestionParser.processHTMLField(question.getFeedback(), false, + qbQuestion.getContentFolderId(), question.getResourcesFolderPath())); + qbQuestion.setPenaltyFactor(0); + int questionId = qbService.generateNextQuestionId(); + qbQuestion.setQuestionId(questionId); + qbQuestion.setVersion(1); - // options are different depending on the type - if (isMultipleChoice || isMarkHedgingType || isVsaType) { + Integer questionMark = question.getScore(); + boolean isMultipleChoice = Question.QUESTION_TYPE_MULTIPLE_CHOICE.equals(question.getType()); + boolean isMarkHedgingType = Question.QUESTION_TYPE_MARK_HEDGING.equals(question.getType()); + boolean isVsaType = Question.QUESTION_TYPE_FILL_IN_BLANK.contentEquals(question.getType()); - // setting answers is very similar in both types, so they were put together here - if (isMarkHedgingType) { - qbQuestion.setType(QbQuestion.TYPE_MARK_HEDGING); + // options are different depending on the type + if (isMultipleChoice || isMarkHedgingType || isVsaType) { - } else if (isMultipleChoice) { - qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE); - qbQuestion.setMultipleAnswersAllowed(false); - qbQuestion.setShuffle(false); - qbQuestion.setPrefixAnswersWithLetters(false); + // setting answers is very similar in both types, so they were put together here + if (isMarkHedgingType) { + qbQuestion.setType(QbQuestion.TYPE_MARK_HEDGING); - } else if (isVsaType) { - qbQuestion.setType(QbQuestion.TYPE_VERY_SHORT_ANSWERS); - qbQuestion.setCaseSensitive(false); - } + } else if (isMultipleChoice) { + qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE); + qbQuestion.setMultipleAnswersAllowed(false); + qbQuestion.setShuffle(false); + qbQuestion.setPrefixAnswersWithLetters(false); - String correctAnswer = null; - if (question.getAnswers() != null) { - TreeSet optionList = new TreeSet<>(); - int orderId = 0; - for (Answer answer : question.getAnswers()) { - String answerText = QuestionParser.processHTMLField(answer.getText(), false, contentFolderID, - question.getResourcesFolderPath()); - if ((correctAnswer != null) && correctAnswer.equals(answerText)) { - log.warn("Skipping an answer with same text as the correct answer: " + answerText); - continue; - } - QbOption option = new QbOption(); - if (isVsaType) { - // convert comma-separated answers to ones accepted by QB VSA questions - answerText = Stream.of(answerText.split(",")).map(String::strip) - .collect(Collectors.joining("\r\n")); - } - option.setName(answerText); - option.setDisplayOrder(orderId++); - option.setFeedback(answer.getFeedback()); - option.setQbQuestion(qbQuestion); + } else if (isVsaType) { + qbQuestion.setType(QbQuestion.TYPE_VERY_SHORT_ANSWERS); + qbQuestion.setCaseSensitive(false); + } - if ((answer.getScore() != null) && (answer.getScore() > 0) && (correctAnswer == null)) { - if (questionMark == null) { - // whatever the correct answer holds, it becomes the question score - questionMark = Double.valueOf(Math.ceil(answer.getScore())).intValue(); + String correctAnswer = null; + if (question.getAnswers() != null) { + TreeSet optionList = new TreeSet<>(); + int orderId = 0; + for (Answer answer : question.getAnswers()) { + String answerText = QuestionParser.processHTMLField(answer.getText(), false, + contentFolderID, question.getResourcesFolderPath()); + if ((correctAnswer != null) && correctAnswer.equals(answerText)) { + log.warn("Skipping an answer with same text as the correct answer: " + answerText); + continue; } - // 100% goes to the correct answer - option.setMaxMark(1); - correctAnswer = answerText; - } else { - option.setMaxMark(0); + QbOption option = new QbOption(); + if (isVsaType) { + // convert comma-separated answers to ones accepted by QB VSA questions + answerText = Stream.of(answerText.split(",")).map(String::strip) + .collect(Collectors.joining("\r\n")); + } + option.setName(answerText); + option.setDisplayOrder(orderId++); + option.setFeedback(answer.getFeedback()); + option.setQbQuestion(qbQuestion); + + if ((answer.getScore() != null) && (answer.getScore() > 0) && (correctAnswer == null)) { + if (questionMark == null) { + // whatever the correct answer holds, it becomes the question score + questionMark = Double.valueOf(Math.ceil(answer.getScore())).intValue(); + } + // 100% goes to the correct answer + option.setMaxMark(1); + correctAnswer = answerText; + } else { + option.setMaxMark(0); + } + + optionList.add(option); } - optionList.add(option); + qbQuestion.setQbOptions(new ArrayList<>(optionList)); } - qbQuestion.setQbOptions(new ArrayList<>(optionList)); - } + if (correctAnswer == null) { + log.warn("No correct answer found for question: " + question.getText()); + continue; + } - if (correctAnswer == null) { - log.warn("No correct answer found for question: " + question.getText()); - continue; - } + } else if (Question.QUESTION_TYPE_MULTIPLE_RESPONSE.equals(question.getType())) { + qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE); + qbQuestion.setMultipleAnswersAllowed(true); + qbQuestion.setShuffle(false); + qbQuestion.setPrefixAnswersWithLetters(false); - } else if (Question.QUESTION_TYPE_MULTIPLE_RESPONSE.equals(question.getType())) { - qbQuestion.setType(QbQuestion.TYPE_MULTIPLE_CHOICE); - qbQuestion.setMultipleAnswersAllowed(true); - qbQuestion.setShuffle(false); - qbQuestion.setPrefixAnswersWithLetters(false); - - if (question.getAnswers() != null) { - float totalScore = 0; - for (Answer answer : question.getAnswers()) { - if ((answer.getScore() != null) && (answer.getScore() > 0)) { - // the question score information is stored as sum of answer scores - totalScore += answer.getScore(); + if (question.getAnswers() != null) { + float totalScore = 0; + for (Answer answer : question.getAnswers()) { + if ((answer.getScore() != null) && (answer.getScore() > 0)) { + // the question score information is stored as sum of answer scores + totalScore += answer.getScore(); + } } - } - if (questionMark == null) { - questionMark = Double.valueOf(Math.round(totalScore)).intValue(); - } + if (questionMark == null) { + questionMark = Double.valueOf(Math.round(totalScore)).intValue(); + } - TreeSet optionList = new TreeSet<>(); - int orderId = 1; - for (Answer answer : question.getAnswers()) { - String answerText = answer.getText(); - QbOption qbOption = new QbOption(); - qbOption.setName(answerText); - qbOption.setDisplayOrder(orderId++); - qbOption.setFeedback(answer.getFeedback()); - qbOption.setQbQuestion(qbQuestion); + TreeSet optionList = new TreeSet<>(); + int orderId = 1; + for (Answer answer : question.getAnswers()) { + String answerText = answer.getText(); + QbOption qbOption = new QbOption(); + qbOption.setName(answerText); + qbOption.setDisplayOrder(orderId++); + qbOption.setFeedback(answer.getFeedback()); + qbOption.setQbQuestion(qbQuestion); - if ((answer.getScore() != null) && (answer.getScore() > 0)) { - // set the factor of score for correct answers - qbOption.setMaxMark(answer.getScore() / totalScore); - } else { - qbOption.setMaxMark(0); + if ((answer.getScore() != null) && (answer.getScore() > 0)) { + // set the factor of score for correct answers + qbOption.setMaxMark(answer.getScore() / totalScore); + } else { + qbOption.setMaxMark(0); + } + + optionList.add(qbOption); } - optionList.add(qbOption); + qbQuestion.setQbOptions(new ArrayList<>(optionList)); } - qbQuestion.setQbOptions(new ArrayList<>(optionList)); - } + } else if (Question.QUESTION_TYPE_TRUE_FALSE.equals(question.getType())) { + qbQuestion.setType(QbQuestion.TYPE_TRUE_FALSE); - } else if (Question.QUESTION_TYPE_TRUE_FALSE.equals(question.getType())) { - qbQuestion.setType(QbQuestion.TYPE_TRUE_FALSE); - - if (question.getAnswers() == null) { - log.warn("Answers missing from true-false question: " + question.getText()); - continue; - } else { - for (Answer answer : question.getAnswers()) { - if ((answer.getScore() != null) && (answer.getScore() > 0)) { - qbQuestion.setCorrectAnswer(Boolean.parseBoolean(answer.getText())); - if (questionMark == null) { - questionMark = Double.valueOf(Math.ceil(answer.getScore())).intValue(); + if (question.getAnswers() == null) { + log.warn("Answers missing from true-false question: " + question.getText()); + continue; + } else { + for (Answer answer : question.getAnswers()) { + if ((answer.getScore() != null) && (answer.getScore() > 0)) { + qbQuestion.setCorrectAnswer(Boolean.parseBoolean(answer.getText())); + if (questionMark == null) { + questionMark = Double.valueOf(Math.ceil(answer.getScore())).intValue(); + } } - } - if (!StringUtils.isBlank(answer.getFeedback())) { - // set feedback for true/false answers - if (Boolean.parseBoolean(answer.getText())) { - qbQuestion.setFeedbackOnCorrect(answer.getFeedback()); - } else { - qbQuestion.setFeedbackOnIncorrect(answer.getFeedback()); + if (!StringUtils.isBlank(answer.getFeedback())) { + // set feedback for true/false answers + if (Boolean.parseBoolean(answer.getText())) { + qbQuestion.setFeedbackOnCorrect(answer.getFeedback()); + } else { + qbQuestion.setFeedbackOnIncorrect(answer.getFeedback()); + } } } } - } - } else if (Question.QUESTION_TYPE_MATCHING.equals(question.getType())) { - qbQuestion.setType(QbQuestion.TYPE_MATCHING_PAIRS); - qbQuestion.setShuffle(true); + } else if (Question.QUESTION_TYPE_MATCHING.equals(question.getType())) { + qbQuestion.setType(QbQuestion.TYPE_MATCHING_PAIRS); + qbQuestion.setShuffle(true); - if (question.getAnswers() != null) { - // the question score information is stored as sum of answer scores - float totalScore = 0; - for (Answer answer : question.getAnswers()) { - if ((answer.getScore() != null) && (answer.getScore() > 0)) { - totalScore += answer.getScore(); + if (question.getAnswers() != null) { + // the question score information is stored as sum of answer scores + float totalScore = 0; + for (Answer answer : question.getAnswers()) { + if ((answer.getScore() != null) && (answer.getScore() > 0)) { + totalScore += answer.getScore(); + } } - } - if (questionMark == null) { - questionMark = Double.valueOf(Math.round(totalScore)).intValue(); - } + if (questionMark == null) { + questionMark = Double.valueOf(Math.round(totalScore)).intValue(); + } - TreeSet optionList = new TreeSet<>(); - int orderId = 1; - for (int answerIndex = 0; answerIndex < question.getAnswers().size(); answerIndex++) { - // QTI allows answers without a match, but LAMS assessment tool does not - Integer matchAnswerIndex = question.getMatchMap() == null ? null - : question.getMatchMap().get(answerIndex); - Answer matchAnswer = (matchAnswerIndex == null) || (question.getMatchAnswers() == null) ? null - : question.getMatchAnswers().get(matchAnswerIndex); - if (matchAnswer != null) { - Answer answer = question.getAnswers().get(answerIndex); - String answerText = answer.getText(); - QbOption assessmentAnswer = new QbOption(); - assessmentAnswer.setMatchingPair(answerText); - assessmentAnswer.setName(matchAnswer.getText()); - assessmentAnswer.setDisplayOrder(orderId++); - assessmentAnswer.setFeedback(answer.getFeedback()); - assessmentAnswer.setQbQuestion(qbQuestion); + TreeSet optionList = new TreeSet<>(); + int orderId = 1; + for (int answerIndex = 0; answerIndex < question.getAnswers().size(); answerIndex++) { + // QTI allows answers without a match, but LAMS assessment tool does not + Integer matchAnswerIndex = question.getMatchMap() == null ? null + : question.getMatchMap().get(answerIndex); + Answer matchAnswer = (matchAnswerIndex == null) || (question.getMatchAnswers() == null) + ? null + : question.getMatchAnswers().get(matchAnswerIndex); + if (matchAnswer != null) { + Answer answer = question.getAnswers().get(answerIndex); + String answerText = answer.getText(); + QbOption assessmentAnswer = new QbOption(); + assessmentAnswer.setMatchingPair(answerText); + assessmentAnswer.setName(matchAnswer.getText()); + assessmentAnswer.setDisplayOrder(orderId++); + assessmentAnswer.setFeedback(answer.getFeedback()); + assessmentAnswer.setQbQuestion(qbQuestion); - optionList.add(assessmentAnswer); + optionList.add(assessmentAnswer); + } } + + qbQuestion.setQbOptions(new ArrayList<>(optionList)); } + } else if (Question.QUESTION_TYPE_ESSAY.equals(question.getType())) { + qbQuestion.setType(QbQuestion.TYPE_ESSAY); + qbQuestion.setAllowRichEditor(false); - qbQuestion.setQbOptions(new ArrayList<>(optionList)); + } else { + log.warn("Unknow QTI question type: " + question.getType()); + continue; } - } else if (Question.QUESTION_TYPE_ESSAY.equals(question.getType())) { - qbQuestion.setType(QbQuestion.TYPE_ESSAY); - qbQuestion.setAllowRichEditor(false); - } else { - log.warn("Unknow QTI question type: " + question.getType()); - continue; - } + qbQuestion.setMaxMark(questionMark == null ? 1 : questionMark); + userManagementService.save(qbQuestion); - qbQuestion.setMaxMark(questionMark == null ? 1 : questionMark); - userManagementService.save(qbQuestion); + if (question.getLearningOutcomes() != null && !question.getLearningOutcomes().isEmpty()) { + for (String learningOutcomeText : question.getLearningOutcomes()) { + learningOutcomeText = learningOutcomeText.strip(); + List learningOutcomes = userManagementService.findByProperty(Outcome.class, "name", + learningOutcomeText); + Outcome learningOutcome = null; + if (learningOutcomes.isEmpty()) { + learningOutcome = outcomeService.createOutcome(learningOutcomeText, + ImsQtiController.getUserDTO().getUserID()); + } else { + learningOutcome = learningOutcomes.get(0); + } - if (question.getLearningOutcomes() != null && !question.getLearningOutcomes().isEmpty()) { - for (String learningOutcomeText : question.getLearningOutcomes()) { - learningOutcomeText = learningOutcomeText.strip(); - List learningOutcomes = userManagementService.findByProperty(Outcome.class, "name", - learningOutcomeText); - Outcome learningOutcome = null; - if (learningOutcomes.isEmpty()) { - learningOutcome = outcomeService.createOutcome(learningOutcomeText, - ImsQtiController.getUserDTO().getUserID()); - } else { - learningOutcome = learningOutcomes.get(0); + OutcomeMapping outcomeMapping = new OutcomeMapping(); + outcomeMapping.setOutcome(learningOutcome); + outcomeMapping.setQbQuestionId(questionId); + userManagementService.save(outcomeMapping); } - - OutcomeMapping outcomeMapping = new OutcomeMapping(); - outcomeMapping.setOutcome(learningOutcome); - outcomeMapping.setQbQuestionId(questionId); - userManagementService.save(outcomeMapping); } + + qbService.addQuestionToCollection(collectionUid, qbQuestion.getQuestionId(), false); } - qbService.addQuestionToCollection(collectionUid, qbQuestion.getQuestionId(), false); - qbQuestionUidsString.append(qbQuestion.getUid()).append(','); if (log.isDebugEnabled()) { Index: lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/web/controller/AuthoringController.java =================================================================== diff -u -rf7937adfea85b6a6976eaa98df8c68db93f9f060 -rc6f5e5e1aeb31463657f9fe739fc889d2d7e196d --- lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/web/controller/AuthoringController.java (.../AuthoringController.java) (revision f7937adfea85b6a6976eaa98df8c68db93f9f060) +++ lams_tool_lamc/src/java/org/lamsfoundation/lams/tool/mc/web/controller/AuthoringController.java (.../AuthoringController.java) (revision c6f5e5e1aeb31463657f9fe739fc889d2d7e196d) @@ -39,6 +39,10 @@ import org.lamsfoundation.lams.qb.model.QbOption; import org.lamsfoundation.lams.qb.model.QbQuestion; import org.lamsfoundation.lams.qb.service.IQbService; +import org.lamsfoundation.lams.questions.Answer; +import org.lamsfoundation.lams.questions.Question; +import org.lamsfoundation.lams.questions.QuestionExporter; +import org.lamsfoundation.lams.questions.QuestionParser; import org.lamsfoundation.lams.tool.ToolAccessMode; import org.lamsfoundation.lams.tool.mc.McAppConstants; import org.lamsfoundation.lams.tool.mc.dto.McOptionDTO; @@ -94,7 +98,7 @@ ToolAccessMode mode = WebUtil.readToolAccessModeAuthorDefaulted(request); return readDatabaseData(mcAuthoringForm, request, mode); } - + /** * Set the defineLater flag so that learners cannot use content while we are editing. This flag is released when * updateContent is called. @@ -106,7 +110,7 @@ return readDatabaseData(mcAuthoringForm, request, ToolAccessMode.TEACHER); } - + /** * Common method for "unspecified" and "defineLater" */ @@ -813,4 +817,50 @@ return optionDtos; } + /** + * Prepares MC questions for QTI packing + */ + @RequestMapping("/exportQTI") + public String exportQTI(HttpServletRequest request, HttpServletResponse response) { + String sessionMapId = request.getParameter(McAppConstants.ATTR_SESSION_MAP_ID); + SessionMap sessionMap = (SessionMap) request.getSession() + .getAttribute(sessionMapId); + request.setAttribute(McAppConstants.ATTR_SESSION_MAP_ID, sessionMapId); + + List questionDtos = (List) sessionMap.get(McAppConstants.QUESTION_DTOS); + List questions = new LinkedList<>(); + + for (McQuestionDTO mcQuestion : questionDtos) { + Question question = new Question(); + + question.setType(Question.QUESTION_TYPE_MULTIPLE_CHOICE); + question.setTitle(mcQuestion.getName()); + question.setText(mcQuestion.getDescription()); + question.setFeedback(mcQuestion.getFeedback()); + + QbQuestion qbQuestion = qbService.getQuestionByUid(mcQuestion.getQbQuestionUid()); + question.setLabel(QuestionParser.UUID_LABEL_PREFIX + qbQuestion.getUuid()); + + List answers = new ArrayList<>(); + + for (McOptionDTO mcAnswer : mcQuestion.getOptionDtos()) { + Answer answer = new Answer(); + answer.setText(mcAnswer.getCandidateAnswer()); + answer.setScore( + "Correct".equalsIgnoreCase(mcAnswer.getCorrect()) ? Float.parseFloat(mcQuestion.getMark()) : 0); + + answers.add(answer); + question.setAnswers(answers); + } + + // put the questionDescription in the right place + questions.add(mcQuestion.getDisplayOrder() - 1, question); + } + + String title = request.getParameter("title"); + QuestionExporter exporter = new QuestionExporter(title, questions.toArray(Question.QUESTION_ARRAY_TYPE)); + exporter.exportQTIPackage(request, response); + + return null; + } } Index: lams_tool_lamc/web/authoring/BasicContent.jsp =================================================================== diff -u -radd24141d43ff78942c251760e1614925db89464 -rc6f5e5e1aeb31463657f9fe739fc889d2d7e196d --- lams_tool_lamc/web/authoring/BasicContent.jsp (.../BasicContent.jsp) (revision add24141d43ff78942c251760e1614925db89464) +++ lams_tool_lamc/web/authoring/BasicContent.jsp (.../BasicContent.jsp) (revision c6f5e5e1aeb31463657f9fe739fc889d2d7e196d) @@ -85,6 +85,13 @@ }); } }); + } + + function exportQTI() { + var frame = document.getElementById("downloadFileDummyIframe"), + title = encodeURIComponent(document.getElementsByName("title")[0].value); + frame.src = 'authoring/exportQTI.do?sessionMapId=${sessionMapId}' + + '&title=' + title; } @@ -130,3 +137,6 @@ + + + Index: lams_tool_lamc/web/authoring/itemlist.jsp =================================================================== diff -u -r35d8b11c4d990a8343f9dad226397d5ae3f21e12 -rc6f5e5e1aeb31463657f9fe739fc889d2d7e196d --- lams_tool_lamc/web/authoring/itemlist.jsp (.../itemlist.jsp) (revision 35d8b11c4d990a8343f9dad226397d5ae3f21e12) +++ lams_tool_lamc/web/authoring/itemlist.jsp (.../itemlist.jsp) (revision c6f5e5e1aeb31463657f9fe739fc889d2d7e196d) @@ -35,6 +35,9 @@ + + +