Index: lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java =================================================================== diff -u -r52f835246c157f47eed9df1d47f9e7bb0c16315d -rd5eba3675f4c182807df0fcb34727ce506206098 --- lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java (.../QuestionsController.java) (revision 52f835246c157f47eed9df1d47f9e7bb0c16315d) +++ lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java (.../QuestionsController.java) (revision d5eba3675f4c182807df0fcb34727ce506206098) @@ -3,6 +3,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.util.Collection; import java.util.Collections; import java.util.Set; import java.util.TreeSet; @@ -109,7 +110,7 @@ InputStream uploadedFileStream = new FileInputStream(file); - Question[] questions; + Collection questions; if (packageName.endsWith(".xml")) { questions = QuestionParser.parseQTIFile(uploadedFileStream, null, limitType); Index: lams_central/src/java/org/lamsfoundation/lams/web/qb/ImsQtiController.java =================================================================== diff -u -r970744e51617c0614313597e042767b43f5a7326 -rd5eba3675f4c182807df0fcb34727ce506206098 --- lams_central/src/java/org/lamsfoundation/lams/web/qb/ImsQtiController.java (.../ImsQtiController.java) (revision 970744e51617c0614313597e042767b43f5a7326) +++ lams_central/src/java/org/lamsfoundation/lams/web/qb/ImsQtiController.java (.../ImsQtiController.java) (revision d5eba3675f4c182807df0fcb34727ce506206098) @@ -168,7 +168,17 @@ if (question.getAnswers() != null) { TreeSet optionList = new TreeSet<>(); int orderId = 0; + float maxScore = 0; + for (Answer answer : question.getAnswers()) { + if (answer.getScore() != null && answer.getScore() > maxScore) { + maxScore = answer.getScore(); + } + } + + questionMark = Double.valueOf(Math.ceil(maxScore)).intValue(); + + for (Answer answer : question.getAnswers()) { String answerText = QuestionParser.processHTMLField(answer.getText(), false, contentFolderID, question.getResourcesFolderPath()); if ((correctAnswer != null) && correctAnswer.equals(answerText)) { @@ -186,16 +196,14 @@ 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(); + // cast exact score to scale 0-1 + if (answer.getScore() != null && answer.getScore() > 0) { + if (correctAnswer == null && answer.getScore() == maxScore) { + correctAnswer = answerText; } - // 100% goes to the correct answer - option.setMaxMark(1); - correctAnswer = answerText; - } else { - option.setMaxMark(0); + float score = Double.valueOf(Math.round(answer.getScore() / maxScore * 100.0) / 100.0) + .floatValue(); + option.setMaxMark(score); } optionList.add(option); Index: lams_central/web/questions/questionChoice.jsp =================================================================== diff -u -r0f295260a6f4f68d391ed74f3a7967fbf841af90 -rd5eba3675f4c182807df0fcb34727ce506206098 --- lams_central/web/questions/questionChoice.jsp (.../questionChoice.jsp) (revision 0f295260a6f4f68d391ed74f3a7967fbf841af90) +++ lams_central/web/questions/questionChoice.jsp (.../questionChoice.jsp) (revision d5eba3675f4c182807df0fcb34727ce506206098) @@ -59,27 +59,12 @@ }); if (anyQuestionsSelected) { - var form = $("#questionForm"); - if (returnURL == '') { - form.css('visibility', 'hidden'); - window.opener.saveQTI(form[0].outerHTML, 'questionForm', callerID); - // needs to be called twice for Chrome to close pop up window - window.close(); - window.close(); - } else { - // this code is not really used at the moment, but it's available - $.ajax({ - type: "POST", - url: returnURL, - data: form.serializeArray(), - success: function(response) { - window.opener.location.reload(); - // needs to be called twice for Chrome to close pop up window - window.close(); - window.close(); - } - }); - } + var form = $("#questionForm").css('visibility', 'hidden'); + window.opener.saveQTI(form[0].outerHTML, 'questionForm', callerID); + // needs to be called twice for Chrome to close pop up window + window.close(); + window.close(); + } else { $('#errorArea').show(); } @@ -117,7 +102,7 @@ $('#selectAll').click(function(){ var checked = $(this).is(':checked'); - $('.question').attr('checked', checked); + $('.question').attr('checked', checked).prop('checked', checked); $('.questionAttribute').attr('disabled', checked ? null : 'disabled'); if (checked) { $('#errorArea').hide('slow'); Index: lams_common/src/java/org/lamsfoundation/lams/qb/model/QbOption.java =================================================================== diff -u -r2188972474f8d186d6811e3dea2e4136be669335 -rd5eba3675f4c182807df0fcb34727ce506206098 --- lams_common/src/java/org/lamsfoundation/lams/qb/model/QbOption.java (.../QbOption.java) (revision 2188972474f8d186d6811e3dea2e4136be669335) +++ lams_common/src/java/org/lamsfoundation/lams/qb/model/QbOption.java (.../QbOption.java) (revision d5eba3675f4c182807df0fcb34727ce506206098) @@ -2,7 +2,6 @@ import java.io.Serializable; -import javax.persistence.Cacheable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; Index: lams_common/src/java/org/lamsfoundation/lams/questions/Question.java =================================================================== diff -u -r71ecf7fae19f2c5bb42f613c3a72b062b145767e -rd5eba3675f4c182807df0fcb34727ce506206098 --- lams_common/src/java/org/lamsfoundation/lams/questions/Question.java (.../Question.java) (revision 71ecf7fae19f2c5bb42f613c3a72b062b145767e) +++ lams_common/src/java/org/lamsfoundation/lams/questions/Question.java (.../Question.java) (revision d5eba3675f4c182807df0fcb34727ce506206098) @@ -145,11 +145,11 @@ } public Integer getScore() { - return score; + return score; } public void setScore(Integer mark) { - this.score = mark; + this.score = mark; } @Override @@ -162,12 +162,9 @@ if (this == obj) { return true; } - if (obj == null) { + if ((obj == null) || !(obj instanceof Question)) { return false; } - if (!(obj instanceof Question)) { - return false; - } Question other = (Question) obj; if (text == null) { if (other.text != null) { Index: lams_common/src/java/org/lamsfoundation/lams/questions/QuestionParser.java =================================================================== diff -u -r0f295260a6f4f68d391ed74f3a7967fbf841af90 -rd5eba3675f4c182807df0fcb34727ce506206098 --- lams_common/src/java/org/lamsfoundation/lams/questions/QuestionParser.java (.../QuestionParser.java) (revision 0f295260a6f4f68d391ed74f3a7967fbf841af90) +++ lams_common/src/java/org/lamsfoundation/lams/questions/QuestionParser.java (.../QuestionParser.java) (revision d5eba3675f4c182807df0fcb34727ce506206098) @@ -28,7 +28,9 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -41,12 +43,17 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; +import org.lamsfoundation.lams.qb.QbUtils; import org.lamsfoundation.lams.util.UploadFileUtil; import org.lamsfoundation.lams.util.WebUtil; +import org.lamsfoundation.lams.util.XMLUtil; import org.lamsfoundation.lams.util.zipfile.ZipFileUtil; import org.lamsfoundation.lams.util.zipfile.ZipFileUtilException; import org.w3c.dom.Document; @@ -70,42 +77,35 @@ // can be anything private static final String TEMP_PACKAGE_NAME_PREFIX = "QTI_PACKAGE_"; private static final Pattern IMAGE_PATTERN = Pattern.compile("\\[IMAGE: ([^\\]]+)\\]"); + private static XPath xPath = XPathFactory.newInstance().newXPath(); public static final String UUID_LABEL_PREFIX = "lams-qb-uuid-"; /** * Extracts questions from IMS QTI zip file. */ - public static Question[] parseQTIPackage(InputStream packageFileStream, Set limitType) + public static Collection parseQTIPackage(InputStream packageFileStream, Set limitType) throws SAXParseException, IOException, SAXException, ParserConfigurationException, ZipFileUtilException { List result = new ArrayList<>(); // unique folder name String tempPackageName = TEMP_PACKAGE_NAME_PREFIX + System.currentTimeMillis(); String tempPackageDirPath = ZipFileUtil.expandZip(packageFileStream, tempPackageName); - try { + try (packageFileStream) { List resourceFiles = QuestionParser.getQTIResourceFiles(tempPackageDirPath); if (resourceFiles.isEmpty()) { log.warn("No resource files found in QTI package"); } else { // extract from every XML file; usually there is just one for (File resourceFile : resourceFiles) { - FileInputStream xmlFileStream = new FileInputStream(resourceFile); - Question[] fileQuestions = null; - try { - fileQuestions = QuestionParser.parseQTIFile(xmlFileStream, tempPackageDirPath, limitType); - } finally { - xmlFileStream.close(); + try (FileInputStream xmlFileStream = new FileInputStream(resourceFile)) { + Collection fileQuestions = QuestionParser.parseQTIFile(xmlFileStream, + tempPackageDirPath, limitType); + result.addAll(fileQuestions); } - if (fileQuestions != null) { - Collections.addAll(result, fileQuestions); - } } } } finally { - // clean up - packageFileStream.close(); - // if there are any images attached, do not delete the exploded ZIP // unfortunately, in this case it stays there until OS does temp dir clean up boolean tempFolderStillNeeded = false; @@ -122,251 +122,264 @@ } } - return result.toArray(Question.QUESTION_ARRAY_TYPE); + return result; } /** * Extracts questions from IMS QTI xml file. */ - public static Question[] parseQTIFile(InputStream xmlFileStream, String resourcesFolderPath, Set limitType) - throws ParserConfigurationException, SAXException, IOException { - List result = new ArrayList<>(); + public static Collection parseQTIFile(InputStream xmlFileStream, String resourcesFolderPath, + Set limitType) throws ParserConfigurationException, SAXException, IOException { + List questions = new ArrayList<>(); DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = docBuilder.parse(xmlFileStream); + // in theory in a single QTI package v1 and v2 items can be mixed NodeList questionItems = doc.getElementsByTagName("item"); - // yes, a label here for convenience - questionLoop: for (int questionItemIndex = 0; questionItemIndex < questionItems - .getLength(); questionItemIndex++) { + for (int questionItemIndex = 0; questionItemIndex < questionItems.getLength(); questionItemIndex++) { Element questionItem = (Element) questionItems.item(questionItemIndex); - NodeList questionItemTypes = questionItem.getElementsByTagName("qmd_itemtype"); - String questionItemType = questionItemTypes.getLength() > 0 - ? ((Text) questionItemTypes.item(0).getChildNodes().item(0)).getData() - : null; - Question question = new Question(); - // check if it is "matching" question type - if ("Matching".equalsIgnoreCase(questionItemType) - && !QuestionParser.isQuestionTypeAcceptable(Question.QUESTION_TYPE_MATCHING, limitType, question)) { - continue; + Question question = QuestionParser.parseQTIItemVesion1(questionItem, resourcesFolderPath, limitType); + if (question != null) { + questions.add(question); } - String questionTitle = questionItem.getAttribute("title"); - question.setTitle(questionTitle); - String questionLabel = questionItem.getAttribute("label"); - if (StringUtils.isNotBlank(questionLabel)) { - question.setLabel(questionLabel); + } + + questionItems = doc.getElementsByTagName("assessmentItem"); + for (int questionItemIndex = 0; questionItemIndex < questionItems.getLength(); questionItemIndex++) { + Element questionItem = (Element) questionItems.item(questionItemIndex); + Question question = QuestionParser.parseQTIItemVesion2(questionItem, resourcesFolderPath, limitType); + if (question != null) { + questions.add(question); } + } - Map answerMap = new TreeMap<>(); - Map matchAnswerMap = null; - boolean textBasedQuestion = false; + return questions; + } - Element presentation = (Element) questionItem.getElementsByTagName("presentation").item(0); - NodeList presentationChildrenList = presentation.getChildNodes(); - // cumberstone parsing, but there is no other way using this API - for (int presentationChildIndex = 0; presentationChildIndex < presentationChildrenList - .getLength(); presentationChildIndex++) { - Node presentationChild = presentationChildrenList.item(presentationChildIndex); - // here is where question data is stored - if ("material".equals(presentationChild.getNodeName())) { - String questionText = QuestionParser.parseMaterialElement(presentationChild, question, - resourcesFolderPath); - if (questionText.trim().startsWith("