diff --git a/src/main/java/org/olat/core/util/filter/impl/NekoHTMLFilter.java b/src/main/java/org/olat/core/util/filter/impl/NekoHTMLFilter.java index 3370d64a25046f25a0daf179dd51875754815287..b2f04a158f2c087eba62d1617def02f3ab2ea4d7 100644 --- a/src/main/java/org/olat/core/util/filter/impl/NekoHTMLFilter.java +++ b/src/main/java/org/olat/core/util/filter/impl/NekoHTMLFilter.java @@ -27,7 +27,8 @@ import java.util.HashSet; import java.util.Set; import org.cyberneko.html.parsers.SAXParser; -import org.olat.core.logging.LogDelegator; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; import org.olat.core.util.filter.Filter; import org.olat.core.util.io.LimitedContentWriter; import org.olat.search.service.document.file.FileDocumentFactory; @@ -45,7 +46,9 @@ import org.xml.sax.helpers.DefaultHandler; * Initial Date: 2 dec. 2009 <br> * @author srosse */ -public class NekoHTMLFilter extends LogDelegator implements Filter { +public class NekoHTMLFilter implements Filter { + + private static final OLog log = Tracing.createLoggerFor(NekoHTMLFilter.class); public static final Set<String> blockTags = new HashSet<String>(); static { @@ -66,13 +69,13 @@ public class NekoHTMLFilter extends LogDelegator implements Filter { parser.parse(new InputSource(new StringReader(original))); return contentHandler.toString(); } catch (SAXException e) { - logError("", e); + log.error("", e); return null; } catch (IOException e) { - logError("", e); + log.error("", e); return null; } catch (Exception e) { - logError("", e); + log.error("", e); return null; } } @@ -86,13 +89,13 @@ public class NekoHTMLFilter extends LogDelegator implements Filter { parser.parse(new InputSource(in)); return contentHandler.getContent(); } catch (SAXException e) { - logError("", e); + log.error("", e); return null; } catch (IOException e) { - logError("", e); + log.error("", e); return null; } catch (Exception e) { - logError("", e); + log.error("", e); return null; } } diff --git a/src/main/java/org/olat/ims/qti21/QTI21Constants.java b/src/main/java/org/olat/ims/qti21/QTI21Constants.java index 7fbbedb7819be9b95e7c7fbac20744dc2507a370..83fb9cbf76652a6cf4badb69fe846b97f81a1478 100644 --- a/src/main/java/org/olat/ims/qti21/QTI21Constants.java +++ b/src/main/java/org/olat/ims/qti21/QTI21Constants.java @@ -47,6 +47,12 @@ public class QTI21Constants { public static final ComplexReferenceIdentifier MAXSCORE_CLX_IDENTIFIER = ComplexReferenceIdentifier.parseString(MAXSCORE); + public static final String MINSCORE = "MINSCORE"; + + public static final Identifier MINSCORE_IDENTIFIER = Identifier.assumedLegal(MINSCORE); + + public static final ComplexReferenceIdentifier MINSCORE_CLX_IDENTIFIER = ComplexReferenceIdentifier.parseString(MINSCORE); + public static final String PASS = "PASS"; public static final Identifier PASS_IDENTIFIER = Identifier.assumedLegal(PASS); @@ -55,8 +61,20 @@ public class QTI21Constants { public static final Identifier FEEDBACKBASIC_IDENTIFIER = Identifier.parseString(FEEDBACKBASIC); - public static final IdentifierValue CORRECT = new IdentifierValue("correct"); + public static final String FEEDBACKMODAL = "FEEDBACKMODAL"; + + public static final Identifier FEEDBACKMODAL_IDENTIFIER = Identifier.parseString(FEEDBACKMODAL); + + public static final String CORRECT = "correct"; + + public static final Identifier CORRECT_IDENTIFIER = Identifier.parseString(CORRECT); + + public static final IdentifierValue CORRECT_IDENTIFIER_VALUE = new IdentifierValue(CORRECT); + + public static final String INCORRECT = "incorrect"; + + public static final Identifier INCORRECT_IDENTIFIER = Identifier.parseString(INCORRECT); - public static final IdentifierValue INCORRECT = new IdentifierValue("incorrect"); + public static final IdentifierValue INCORRECT_IDENTIFIER_VALUE = new IdentifierValue(INCORRECT); } diff --git a/src/main/java/org/olat/ims/qti21/QTI21Service.java b/src/main/java/org/olat/ims/qti21/QTI21Service.java index a3bbceb7aa32ace564d688d8723e3c62a7487121..05d33efa148bd21b4419fbd9d3525423290c7261 100644 --- a/src/main/java/org/olat/ims/qti21/QTI21Service.java +++ b/src/main/java/org/olat/ims/qti21/QTI21Service.java @@ -37,6 +37,7 @@ import org.olat.repository.RepositoryEntryRef; import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; import uk.ac.ed.ph.jqtiplus.node.result.AssessmentResult; import uk.ac.ed.ph.jqtiplus.notification.NotificationRecorder; +import uk.ac.ed.ph.jqtiplus.reading.QtiXmlReader; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentObject; import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; import uk.ac.ed.ph.jqtiplus.state.ItemSessionState; @@ -59,6 +60,8 @@ public interface QTI21Service { */ public QtiSerializer qtiSerializer(); + public QtiXmlReader qtiXmlReader(); + /** * The manager for custom extensions to QTI (MathExtensio ) * @return diff --git a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java index 60ff6c6330e1e4fc6c60f12cc6d3cbb047c57456..8bf959fddfafe0610a8b2a3c1a9529569e6ace32 100644 --- a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java +++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java @@ -179,6 +179,10 @@ public class QTI21ServiceImpl implements QTI21Service, InitializingBean, Disposa return new QtiSerializer(jqtiExtensionManager()); } + @Override + public QtiXmlReader qtiXmlReader() { + return new QtiXmlReader(jqtiExtensionManager()); + } @SuppressWarnings("unchecked") diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentBuilderHelper.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentBuilderHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..a385c2adef2b8862b0ebbf39363ceb89ef23ce70 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentBuilderHelper.java @@ -0,0 +1,123 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamResult; + +import org.olat.core.gui.render.StringOutput; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; +import uk.ac.ed.ph.jqtiplus.exception.QtiModelException; +import uk.ac.ed.ph.jqtiplus.node.LoadingContext; +import uk.ac.ed.ph.jqtiplus.node.content.ItemBody; +import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; +import uk.ac.ed.ph.jqtiplus.node.content.basic.FlowStatic; +import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; + +/** + * + * Initial date: 09.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class AssessmentBuilderHelper { + + private static final OLog log = Tracing.createLoggerFor(AssessmentBuilderHelper.class); + + private final QtiSerializer qtiSerializer; + + public AssessmentBuilderHelper() { + JqtiExtensionManager jqtiExtensionManager = new JqtiExtensionManager(); + qtiSerializer = new QtiSerializer(jqtiExtensionManager); + } + + public AssessmentBuilderHelper(QtiSerializer qtiSerializer) { + this.qtiSerializer = qtiSerializer; + } + + public String toString(List<FlowStatic> statics) { + StringOutput sb = new StringOutput(); + if(statics != null && statics.size() > 0) { + for(FlowStatic flowStatic:statics) { + qtiSerializer.serializeJqtiObject(flowStatic, new StreamResult(sb)); + } + } + return sb.toString(); + } + + public List<Block> parseHtml(String html) { + //tinymce bad habits + if(html.startsWith("<p> ")) { + html = html.replace("<p> ", "<p>"); + } + Document document = htmlToDOM("<html>" + html + "</html>"); + LoadingContext context = new HTMLLoadingContext(); + + ItemBody helper = new ItemBody(null); + helper.load(document.getDocumentElement(), context); + return helper.getBlocks(); + } + + /** + * This method use the standard XML parser. It's not really + * good but QTIWorks want DOM Level 2 elements. + * + * @param content + * @return + */ + private Document htmlToDOM(String content) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new ByteArrayInputStream(content.getBytes())); + return doc; + } catch (ParserConfigurationException | SAXException | IOException e) { + log.error("", e); + return null; + } + } + + private static final class HTMLLoadingContext implements LoadingContext { + + @Override + public JqtiExtensionManager getJqtiExtensionManager() { + return null; + } + + @Override + public void modelBuildingError(QtiModelException exception, Node badNode) { + // + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..c206f27fd8028e174b9bd45957fe5a1abd245ce7 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilder.java @@ -0,0 +1,274 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import static org.olat.ims.qti21.QTI21Constants.MAXSCORE_IDENTIFIER; +import static org.olat.ims.qti21.QTI21Constants.MINSCORE_IDENTIFIER; + +import java.util.ArrayList; +import java.util.List; + +import org.olat.ims.qti21.QTI21Constants; + +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.ModalFeedback; +import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseCondition; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseProcessing; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseRule; +import uk.ac.ed.ph.jqtiplus.node.outcome.declaration.OutcomeDeclaration; +import uk.ac.ed.ph.jqtiplus.node.shared.declaration.DefaultValue; +import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; +import uk.ac.ed.ph.jqtiplus.value.FloatValue; +import uk.ac.ed.ph.jqtiplus.value.Value; + +/** + * + * + * Initial date: 08.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public abstract class AssessmentItemBuilder { + + protected final AssessmentItem assessmentItem; + protected final QtiSerializer qtiSerializer; + protected final AssessmentBuilderHelper builderHelper; + + private ScoreBuilder minScoreBuilder; + private ScoreBuilder maxScoreBuilder; + + private ModalFeedbackBuilder correctFeedback; + private ModalFeedbackBuilder incorrectFeedback; + private List<ModalFeedbackBuilder> additionalFeedbacks = new ArrayList<>(); + + public AssessmentItemBuilder(AssessmentItem assessmentItem, QtiSerializer qtiSerializer) { + this.assessmentItem = assessmentItem; + this.qtiSerializer = qtiSerializer; + builderHelper = new AssessmentBuilderHelper(qtiSerializer); + extract(); + } + + protected void extract() { + extractMinScore(); + extractMaxScore(); + extractModalFeedbacks(); + } + + private void extractMinScore() { + OutcomeDeclaration outcomeDeclaration = assessmentItem.getOutcomeDeclaration(MINSCORE_IDENTIFIER); + if(outcomeDeclaration != null) { + DefaultValue defaultValue = outcomeDeclaration.getDefaultValue(); + if(defaultValue != null) { + Value minScoreValue = defaultValue.evaluate(); + if(minScoreValue instanceof FloatValue) { + Double minScore = new Double(((FloatValue)minScoreValue).doubleValue()); + minScoreBuilder = new ScoreBuilder(minScore, outcomeDeclaration); + } + } + } + } + + private void extractMaxScore() { + OutcomeDeclaration outcomeDeclaration = assessmentItem.getOutcomeDeclaration(MAXSCORE_IDENTIFIER); + if(outcomeDeclaration != null) { + DefaultValue defaultValue = outcomeDeclaration.getDefaultValue(); + if(defaultValue != null) { + Value maxScoreValue = defaultValue.evaluate(); + if(maxScoreValue instanceof FloatValue) { + Double maxScore = new Double(((FloatValue)maxScoreValue).doubleValue()); + maxScoreBuilder = new ScoreBuilder(maxScore, outcomeDeclaration); + } + } + } + } + + private void extractModalFeedbacks() { + List<ModalFeedback> feedbacks = assessmentItem.getModalFeedbacks(); + for(ModalFeedback feedback:feedbacks) { + ModalFeedbackBuilder feedbackBuilder = new ModalFeedbackBuilder(assessmentItem, feedback); + if(feedbackBuilder.isCorrectRule()) { + correctFeedback = feedbackBuilder; + } else if(feedbackBuilder.isIncorrectRule()) { + incorrectFeedback = feedbackBuilder; + } else { + additionalFeedbacks.add(feedbackBuilder); + } + } + } + + public String getTitle() { + return assessmentItem.getTitle(); + } + + public void setTitle(String title) { + assessmentItem.setTitle(title); + } + + public ScoreBuilder getMinScoreBuilder() { + return minScoreBuilder; + } + + public void setMinScore(Double minScore) { + if(minScoreBuilder == null) { + minScoreBuilder = new ScoreBuilder(minScore, null); + } else { + minScoreBuilder.setScore(minScore); + } + } + + public ScoreBuilder getMaxScoreBuilder() { + return maxScoreBuilder; + } + + public void setMaxScore(Double maxScore) { + if(maxScoreBuilder == null) { + maxScoreBuilder = new ScoreBuilder(maxScore, null); + } else { + maxScoreBuilder.setScore(maxScore); + } + } + + public ModalFeedbackBuilder getCorrectFeedback() { + return correctFeedback; + } + + public ModalFeedbackBuilder createCorrectFeedback() { + correctFeedback = new ModalFeedbackBuilder(assessmentItem, null); + + + return correctFeedback; + } + + public ModalFeedbackBuilder getIncorrectFeedback() { + return incorrectFeedback; + } + + public AssessmentBuilderHelper getHelper() { + return builderHelper; + } + + public final void build() { + List<OutcomeDeclaration> outcomeDeclarations = assessmentItem.getOutcomeDeclarations(); + outcomeDeclarations.clear(); + + ResponseProcessing responseProcessing = assessmentItem.getResponseProcessing(); + List<ResponseRule> responseRules = responseProcessing.getResponseRules(); + responseRules.clear(); + + List<ResponseDeclaration> responseDeclarations = assessmentItem.getResponseDeclarations(); + responseDeclarations.clear(); + + buildResponseDeclaration(); + buildItemBody(); + buildModalFeedback(outcomeDeclarations, responseRules); + buildScores(outcomeDeclarations, responseRules); + buildMainScoreRule(responseRules); + } + + protected void buildResponseDeclaration() { + // + } + + protected void buildItemBody() { + // + } + + protected abstract void buildMainScoreRule(List<ResponseRule> responseRules); + + protected void buildModalFeedback(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) { + //add feedbackbasic and feedbackmodal outcomes + if(correctFeedback != null || incorrectFeedback != null || additionalFeedbacks.size() > 0) { + OutcomeDeclaration basicOutcomeDeclaration = AssessmentItemFactory + .createOutcomeDeclarationForFeedbackBasic(assessmentItem); + outcomeDeclarations.add(basicOutcomeDeclaration); + + OutcomeDeclaration modalOutcomeDeclaration = AssessmentItemFactory + .createOutcomeDeclarationForFeedbackModal(assessmentItem); + outcomeDeclarations.add(modalOutcomeDeclaration); + } + + //add modal + List<ModalFeedback> modalFeedbacks = assessmentItem.getModalFeedbacks(); + modalFeedbacks.clear(); + + if(correctFeedback != null) { + ModalFeedback correctModalFeedback = AssessmentItemFactory + .createModalFeedback(assessmentItem, correctFeedback.getIdentifier(), + correctFeedback.getTitle(), correctFeedback.getText()); + modalFeedbacks.add(correctModalFeedback); + + ResponseCondition feedbackCondition = AssessmentItemFactory + .createModalFeedbackBasicRule(assessmentItem.getResponseProcessing(), correctFeedback.getIdentifier(), QTI21Constants.CORRECT); + responseRules.add(feedbackCondition); + } + + if(incorrectFeedback != null) { + ModalFeedback incorrectModalFeedback = AssessmentItemFactory + .createModalFeedback(assessmentItem, incorrectFeedback.getIdentifier(), + incorrectFeedback.getTitle(), incorrectFeedback.getText()); + modalFeedbacks.add(incorrectModalFeedback); + + ResponseCondition feedbackCondition = AssessmentItemFactory + .createModalFeedbackBasicRule(assessmentItem.getResponseProcessing(), incorrectFeedback.getIdentifier(), QTI21Constants.INCORRECT); + responseRules.add(feedbackCondition); + } + } + + /** + * Add outcome declaration for score, min. score and mx. score. + * and the response rules which ensure that the score is between + * the max. score and min. score. + * + * @param outcomeDeclarations + * @param responseRules + */ + protected void buildScores(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) { + if((getMinScoreBuilder() != null && getMinScoreBuilder().getScore() != null) + || (getMaxScoreBuilder() != null && getMaxScoreBuilder().getScore() != null)) { + + OutcomeDeclaration outcomeDeclaration = AssessmentItemFactory + .createOutcomeDeclarationForScore(assessmentItem); + outcomeDeclarations.add(outcomeDeclaration); + } + + if(getMinScoreBuilder() != null && getMinScoreBuilder().getScore() != null) { + OutcomeDeclaration outcomeDeclaration = AssessmentItemFactory + .createOutcomeDeclarationForMinScore(assessmentItem, getMinScoreBuilder().getScore().doubleValue()); + outcomeDeclarations.add(outcomeDeclaration); + + //ensure that the score is not smaller than min. score value + ResponseRule minScoreBoundResponseRule = AssessmentItemFactory + .createMinScoreBoundLimitRule(assessmentItem.getResponseProcessing()); + responseRules.add(minScoreBoundResponseRule); + } + + if(getMaxScoreBuilder() != null && getMaxScoreBuilder().getScore() != null) { + OutcomeDeclaration outcomeDeclaration = AssessmentItemFactory + .createOutcomeDeclarationForMaxScore(assessmentItem, getMaxScoreBuilder().getScore().doubleValue()); + outcomeDeclarations.add(outcomeDeclaration); + + //ensure that the score is not bigger than the max. score value + ResponseRule maxScoreBoundResponseRule = AssessmentItemFactory + .createMaxScoreBoundLimitRule(assessmentItem.getResponseProcessing()); + responseRules.add(maxScoreBoundResponseRule); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java index 345c0f323688949997c5712fa709b9495071bc50..8d3113436dd23b8204a5c88bdab40181a8196975 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java @@ -19,6 +19,14 @@ */ package org.olat.ims.qti21.model.xml; +import static org.olat.ims.qti21.QTI21Constants.MAXSCORE_CLX_IDENTIFIER; +import static org.olat.ims.qti21.QTI21Constants.MINSCORE_CLX_IDENTIFIER; +import static org.olat.ims.qti21.QTI21Constants.SCORE_CLX_IDENTIFIER; +import static org.olat.ims.qti21.QTI21Constants.SCORE_IDENTIFIER; + +import java.util.ArrayList; +import java.util.List; + import org.olat.core.helpers.Settings; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.model.IdentifierGenerator; @@ -27,21 +35,27 @@ import uk.ac.ed.ph.jqtiplus.group.NodeGroupList; import uk.ac.ed.ph.jqtiplus.group.item.ItemBodyGroup; import uk.ac.ed.ph.jqtiplus.group.item.interaction.PromptGroup; import uk.ac.ed.ph.jqtiplus.group.item.interaction.choice.SimpleChoiceGroup; -import uk.ac.ed.ph.jqtiplus.group.item.response.declaration.ResponseDeclarationGroup; import uk.ac.ed.ph.jqtiplus.group.item.response.processing.ResponseProcessingGroup; import uk.ac.ed.ph.jqtiplus.group.outcome.declaration.OutcomeDeclarationGroup; import uk.ac.ed.ph.jqtiplus.node.QtiNode; import uk.ac.ed.ph.jqtiplus.node.content.ItemBody; +import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; +import uk.ac.ed.ph.jqtiplus.node.content.basic.FlowStatic; import uk.ac.ed.ph.jqtiplus.node.content.basic.TextRun; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.P; import uk.ac.ed.ph.jqtiplus.node.expression.general.BaseValue; import uk.ac.ed.ph.jqtiplus.node.expression.general.Correct; import uk.ac.ed.ph.jqtiplus.node.expression.general.Variable; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.And; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.Gt; import uk.ac.ed.ph.jqtiplus.node.expression.operator.IsNull; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.Lt; import uk.ac.ed.ph.jqtiplus.node.expression.operator.Match; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.Multiple; import uk.ac.ed.ph.jqtiplus.node.expression.operator.Sum; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse; +import uk.ac.ed.ph.jqtiplus.node.item.ModalFeedback; import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleChoice; import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; @@ -50,10 +64,13 @@ import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElse; import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElseIf; import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseIf; import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseProcessing; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseRule; import uk.ac.ed.ph.jqtiplus.node.item.response.processing.SetOutcomeValue; import uk.ac.ed.ph.jqtiplus.node.outcome.declaration.OutcomeDeclaration; import uk.ac.ed.ph.jqtiplus.node.shared.FieldValue; import uk.ac.ed.ph.jqtiplus.node.shared.declaration.DefaultValue; +import uk.ac.ed.ph.jqtiplus.node.test.View; +import uk.ac.ed.ph.jqtiplus.node.test.VisibilityMode; import uk.ac.ed.ph.jqtiplus.types.ComplexReferenceIdentifier; import uk.ac.ed.ph.jqtiplus.types.Identifier; import uk.ac.ed.ph.jqtiplus.value.BaseType; @@ -81,23 +98,11 @@ public class AssessmentItemFactory { NodeGroupList nodeGroups = assessmentItem.getNodeGroups(); - String responseDeclarationId = "RESPONSE_1"; - //define correct answer - ResponseDeclarationGroup responseDeclarations = nodeGroups.getResponseDeclarationGroup(); - ResponseDeclaration responseDeclaration = new ResponseDeclaration(assessmentItem); - responseDeclaration.setIdentifier(Identifier.parseString(responseDeclarationId)); - responseDeclaration.setCardinality(Cardinality.SINGLE); - responseDeclaration.setBaseType(BaseType.IDENTIFIER); - responseDeclarations.getResponseDeclarations().add(responseDeclaration); - - CorrectResponse correctResponse = new CorrectResponse(responseDeclaration); - responseDeclaration.setCorrectResponse(correctResponse); - + Identifier responseDeclarationId = Identifier.assumedLegal("RESPONSE_1"); Identifier correctResponseId = IdentifierGenerator.newAsIdentifier(); - FieldValue fieldValue = new FieldValue(correctResponse); - IdentifierValue identifierValue = new IdentifierValue(correctResponseId); - fieldValue.setSingleValue(identifierValue); - correctResponse.getFieldValues().add(fieldValue); + //define correct answer + ResponseDeclaration responseDeclaration = createSingleChoiceCorrectResponseDeclaration(assessmentItem, responseDeclarationId, correctResponseId); + nodeGroups.getResponseDeclarationGroup().getResponseDeclarations().add(responseDeclaration); //outcomes OutcomeDeclarationGroup outcomeDeclarations = nodeGroups.getOutcomeDeclarationGroup(); @@ -107,11 +112,11 @@ public class AssessmentItemFactory { outcomeDeclarations.getOutcomeDeclarations().add(scoreOutcomeDeclaration); // outcome max score - OutcomeDeclaration maxScoreOutcomeDeclaration = createOutcomeDeclarationForMaxScore(assessmentItem); + OutcomeDeclaration maxScoreOutcomeDeclaration = createOutcomeDeclarationForMaxScore(assessmentItem, 1.0d); outcomeDeclarations.getOutcomeDeclarations().add(maxScoreOutcomeDeclaration); // outcome feedback - OutcomeDeclaration feedbackOutcomeDeclaration = createOutcomeDeclarationForFeedback(assessmentItem); + OutcomeDeclaration feedbackOutcomeDeclaration = createOutcomeDeclarationForFeedbackBasic(assessmentItem); outcomeDeclarations.getOutcomeDeclarations().add(feedbackOutcomeDeclaration); //the single choice interaction @@ -126,7 +131,7 @@ public class AssessmentItemFactory { ChoiceInteraction choiceInteraction = new ChoiceInteraction(itemBody); choiceInteraction.setMaxChoices(1); choiceInteraction.setShuffle(true); - choiceInteraction.setResponseIdentifier(Identifier.parseString(responseDeclarationId)); + choiceInteraction.setResponseIdentifier(responseDeclarationId); itemBodyGroup.getItemBody().getBlocks().add(choiceInteraction); PromptGroup prompts = new PromptGroup(choiceInteraction); @@ -150,6 +155,22 @@ public class AssessmentItemFactory { return assessmentItem; } + public static ResponseDeclaration createSingleChoiceCorrectResponseDeclaration(AssessmentItem assessmentItem, Identifier declarationId, Identifier correctResponseId) { + ResponseDeclaration responseDeclaration = new ResponseDeclaration(assessmentItem); + responseDeclaration.setIdentifier(declarationId); + responseDeclaration.setCardinality(Cardinality.SINGLE); + responseDeclaration.setBaseType(BaseType.IDENTIFIER); + + CorrectResponse correctResponse = new CorrectResponse(responseDeclaration); + responseDeclaration.setCorrectResponse(correctResponse); + + FieldValue fieldValue = new FieldValue(correctResponse); + IdentifierValue identifierValue = new IdentifierValue(correctResponseId); + fieldValue.setSingleValue(identifierValue); + correctResponse.getFieldValues().add(fieldValue); + return responseDeclaration; + } + public static OutcomeDeclaration createOutcomeDeclarationForScore(AssessmentItem assessmentItem) { OutcomeDeclaration scoreOutcomeDeclaration = new OutcomeDeclaration(assessmentItem); scoreOutcomeDeclaration.setIdentifier(QTI21Constants.SCORE_IDENTIFIER); @@ -165,7 +186,49 @@ public class AssessmentItemFactory { return scoreOutcomeDeclaration; } - public static OutcomeDeclaration createOutcomeDeclarationForMaxScore(AssessmentItem assessmentItem) { + /** + * Rule which ensure that the final score is not above the max. score value. + */ + public static ResponseRule createMaxScoreBoundLimitRule(ResponseProcessing responseProcessing) { + /* + <responseCondition> + <responseIf> + <gt> + <variable identifier="SCORE" /><variable identifier="MAXSCORE" /> + </gt> + <setOutcomeValue identifier="SCORE"> + <variable identifier="MAXSCORE" /> + </setOutcomeValue> + </responseIf> + </responseCondition> + */ + ResponseCondition rule = new ResponseCondition(responseProcessing); + ResponseIf responseIf = new ResponseIf(rule); + rule.setResponseIf(responseIf); + + Gt gt = new Gt(responseIf); + responseIf.setExpression(gt); + + Variable scoreVar = new Variable(gt); + scoreVar.setIdentifier(SCORE_CLX_IDENTIFIER); + gt.getExpressions().add(scoreVar); + + Variable maxScoreVar = new Variable(gt); + maxScoreVar.setIdentifier(MAXSCORE_CLX_IDENTIFIER); + gt.getExpressions().add(maxScoreVar); + + SetOutcomeValue setOutcomeValue = new SetOutcomeValue(responseIf); + setOutcomeValue.setIdentifier(SCORE_IDENTIFIER); + + Variable maxScoreOutcomeVar = new Variable(setOutcomeValue); + maxScoreOutcomeVar.setIdentifier(MAXSCORE_CLX_IDENTIFIER); + setOutcomeValue.setExpression(maxScoreOutcomeVar); + responseIf.getResponseRules().add(setOutcomeValue); + + return rule; + } + + public static OutcomeDeclaration createOutcomeDeclarationForMaxScore(AssessmentItem assessmentItem, double maxScore) { OutcomeDeclaration maxScoreOutcomeDeclaration = new OutcomeDeclaration(assessmentItem); maxScoreOutcomeDeclaration.setIdentifier(QTI21Constants.MAXSCORE_IDENTIFIER); maxScoreOutcomeDeclaration.setCardinality(Cardinality.SINGLE); @@ -174,13 +237,74 @@ public class AssessmentItemFactory { DefaultValue maxScoreDefaultVal = new DefaultValue(maxScoreOutcomeDeclaration); maxScoreOutcomeDeclaration.setDefaultValue(maxScoreDefaultVal); - FieldValue maxScoreDefaultFieldVal = new FieldValue(maxScoreDefaultVal, new FloatValue(1.0f)); + FieldValue maxScoreDefaultFieldVal = new FieldValue(maxScoreDefaultVal, new FloatValue(maxScore)); + maxScoreDefaultVal.getFieldValues().add(maxScoreDefaultFieldVal); + + return maxScoreOutcomeDeclaration; + } + + /** + * Rule which ensure that the final score is not under the min. score value. + */ + public static ResponseRule createMinScoreBoundLimitRule(ResponseProcessing responseProcessing) { + /* + <responseCondition> + <responseIf> + <lt> + <variable identifier="SCORE" /><variable identifier="MINSCORE" /> + </lt> + <setOutcomeValue identifier="SCORE"> + <variable identifier="MINSCORE" /> + </setOutcomeValue> + </responseIf> + </responseCondition> + */ + ResponseCondition rule = new ResponseCondition(responseProcessing); + ResponseIf responseIf = new ResponseIf(rule); + rule.setResponseIf(responseIf); + + Lt lt = new Lt(responseIf); + responseIf.setExpression(lt); + + Variable scoreVar = new Variable(lt); + scoreVar.setIdentifier(SCORE_CLX_IDENTIFIER); + lt.getExpressions().add(scoreVar); + + Variable minScoreVar = new Variable(lt); + minScoreVar.setIdentifier(MINSCORE_CLX_IDENTIFIER); + lt.getExpressions().add(minScoreVar); + + SetOutcomeValue setOutcomeValue = new SetOutcomeValue(responseIf); + setOutcomeValue.setIdentifier(SCORE_IDENTIFIER); + + Variable minScoreOutcomeVar = new Variable(setOutcomeValue); + minScoreOutcomeVar.setIdentifier(MINSCORE_CLX_IDENTIFIER); + setOutcomeValue.setExpression(minScoreOutcomeVar); + responseIf.getResponseRules().add(setOutcomeValue); + + return rule; + } + + public static OutcomeDeclaration createOutcomeDeclarationForMinScore(AssessmentItem assessmentItem, double minScore) { + OutcomeDeclaration maxScoreOutcomeDeclaration = new OutcomeDeclaration(assessmentItem); + maxScoreOutcomeDeclaration.setIdentifier(QTI21Constants.MINSCORE_IDENTIFIER); + maxScoreOutcomeDeclaration.setCardinality(Cardinality.SINGLE); + maxScoreOutcomeDeclaration.setBaseType(BaseType.FLOAT); + + List<View> views = new ArrayList<>(); + views.add(View.TEST_CONSTRUCTOR); + maxScoreOutcomeDeclaration.setViews(views); + + DefaultValue maxScoreDefaultVal = new DefaultValue(maxScoreOutcomeDeclaration); + maxScoreOutcomeDeclaration.setDefaultValue(maxScoreDefaultVal); + + FieldValue maxScoreDefaultFieldVal = new FieldValue(maxScoreDefaultVal, new FloatValue(minScore)); maxScoreDefaultVal.getFieldValues().add(maxScoreDefaultFieldVal); return maxScoreOutcomeDeclaration; } - public static OutcomeDeclaration createOutcomeDeclarationForFeedback(AssessmentItem assessmentItem) { + public static OutcomeDeclaration createOutcomeDeclarationForFeedbackBasic(AssessmentItem assessmentItem) { OutcomeDeclaration feedbackOutcomeDeclaration = new OutcomeDeclaration(assessmentItem); feedbackOutcomeDeclaration.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); feedbackOutcomeDeclaration.setCardinality(Cardinality.SINGLE); @@ -192,10 +316,125 @@ public class AssessmentItemFactory { FieldValue feedbackDefaultFieldVal = new FieldValue(feedbackDefaultVal, new IdentifierValue("empty")); feedbackDefaultVal.getFieldValues().add(feedbackDefaultFieldVal); + List<View> views = new ArrayList<>(); + views.add(View.TEST_CONSTRUCTOR); + feedbackOutcomeDeclaration.setViews(views); + + return feedbackOutcomeDeclaration; + } + + public static OutcomeDeclaration createOutcomeDeclarationForFeedbackModal(AssessmentItem assessmentItem) { + OutcomeDeclaration feedbackOutcomeDeclaration = new OutcomeDeclaration(assessmentItem); + feedbackOutcomeDeclaration.setIdentifier(QTI21Constants.FEEDBACKMODAL_IDENTIFIER); + feedbackOutcomeDeclaration.setCardinality(Cardinality.MULTIPLE); + feedbackOutcomeDeclaration.setBaseType(BaseType.IDENTIFIER); + + List<View> views = new ArrayList<>(); + views.add(View.TEST_CONSTRUCTOR); + feedbackOutcomeDeclaration.setViews(views); + return feedbackOutcomeDeclaration; } - public static ResponseProcessing createResponseProcessing(AssessmentItem assessmentItem, String responseId) { + public static ChoiceInteraction createSingleChoiceInteraction(AssessmentItem assessmentItem, Identifier responseDeclarationId) { + ChoiceInteraction choiceInteraction = new ChoiceInteraction(assessmentItem.getItemBody()); + choiceInteraction.setMaxChoices(1); + choiceInteraction.setShuffle(true); + choiceInteraction.setResponseIdentifier(responseDeclarationId); + + PromptGroup prompts = new PromptGroup(choiceInteraction); + choiceInteraction.getNodeGroups().add(prompts); + + SimpleChoiceGroup singleChoices = new SimpleChoiceGroup(choiceInteraction); + choiceInteraction.getNodeGroups().add(singleChoices); + return choiceInteraction; + } + + public static ModalFeedback createModalFeedback(AssessmentItem assessmentItem, Identifier identifier, String title, String text) { + /* + <modalFeedback identifier="Feedback1041659806" outcomeIdentifier="FEEDBACKMODAL" showHide="show" title="Wrong answer"> + <p>Feedback answer</p> + </modalFeedback> + */ + + ModalFeedback modalFeedback = new ModalFeedback(assessmentItem); + modalFeedback.setIdentifier(identifier); + modalFeedback.setOutcomeIdentifier(QTI21Constants.FEEDBACKMODAL_IDENTIFIER); + modalFeedback.setVisibilityMode(VisibilityMode.parseVisibilityMode("show")); + modalFeedback.getAttributes().getStringAttribute(ModalFeedback.ATTR_TITLE_NAME).setValue(title); + + List<Block> blocks = new AssessmentBuilderHelper().parseHtml(text); + for(Block block:blocks) { + if(block instanceof FlowStatic) { + modalFeedback.getFlowStatics().add((FlowStatic)block); + } + } + + return modalFeedback; + } + + public static ResponseCondition createModalFeedbackBasicRule(ResponseProcessing responseProcessing, Identifier feedbackIdentifier, String inCorrect) { + ResponseCondition rule = new ResponseCondition(responseProcessing); + /* + <responseIf> + <and> + <match> + <baseValue baseType="identifier">correct</baseValue> + <variable identifier="FEEDBACKBASIC" /> + </match> + </and> + <setOutcomeValue identifier="FEEDBACKMODAL"> + <multiple> + <variable identifier="FEEDBACKMODAL" /> + <baseValue baseType="identifier">Feedback261171147</baseValue> + </multiple> + </setOutcomeValue> + </responseIf> + */ + + ResponseIf responseIf = new ResponseIf(rule); + rule.setResponseIf(responseIf); + + {//rule + And and = new And(responseIf); + responseIf.getExpressions().add(and); + + Match match = new Match(and); + and.getExpressions().add(match); + + BaseValue feedbackVal = new BaseValue(match); + feedbackVal.setBaseTypeAttrValue(BaseType.IDENTIFIER); + feedbackVal.setSingleValue(new IdentifierValue(inCorrect)); + match.getExpressions().add(feedbackVal); + + Variable variable = new Variable(match); + variable.setIdentifier(ComplexReferenceIdentifier.parseString(QTI21Constants.FEEDBACKBASIC)); + match.getExpressions().add(variable); + } + + {//outcome + SetOutcomeValue feedbackVar = new SetOutcomeValue(responseIf); + feedbackVar.setIdentifier(QTI21Constants.FEEDBACKMODAL_IDENTIFIER); + + Multiple multiple = new Multiple(feedbackVar); + feedbackVar.setExpression(multiple); + + Variable variable = new Variable(multiple); + variable.setIdentifier(ComplexReferenceIdentifier.parseString(QTI21Constants.FEEDBACKMODAL)); + multiple.getExpressions().add(variable); + + BaseValue feedbackVal = new BaseValue(feedbackVar); + feedbackVal.setBaseTypeAttrValue(BaseType.IDENTIFIER); + feedbackVal.setSingleValue(new IdentifierValue(feedbackIdentifier)); + multiple.getExpressions().add(feedbackVal); + + responseIf.getResponseRules().add(feedbackVar); + } + + return rule; + } + + public static ResponseProcessing createResponseProcessing(AssessmentItem assessmentItem, Identifier responseId) { ResponseProcessing responseProcessing = new ResponseProcessing(assessmentItem); ResponseCondition rule = new ResponseCondition(responseProcessing); @@ -208,7 +447,7 @@ public class AssessmentItemFactory { responseIf.getExpressions().add(isNull); Variable variable = new Variable(isNull); - variable.setIdentifier(ComplexReferenceIdentifier.parseString(responseId)); + variable.setIdentifier(ComplexReferenceIdentifier.parseString(responseId.toString())); isNull.getExpressions().add(variable); { @@ -231,11 +470,11 @@ public class AssessmentItemFactory { responseElseIf.getExpressions().add(match); Variable responseVar = new Variable(match); - responseVar.setIdentifier(ComplexReferenceIdentifier.parseString(responseId)); + responseVar.setIdentifier(ComplexReferenceIdentifier.parseString(responseId.toString())); match.getExpressions().add(responseVar); Correct correct = new Correct(match); - correct.setIdentifier(ComplexReferenceIdentifier.parseString(responseId)); + correct.setIdentifier(ComplexReferenceIdentifier.parseString(responseId.toString())); match.getExpressions().add(correct); } @@ -263,7 +502,7 @@ public class AssessmentItemFactory { correctFeedbackVar.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); BaseValue correctFeedbackVal = new BaseValue(correctFeedbackVar); correctFeedbackVal.setBaseTypeAttrValue(BaseType.IDENTIFIER); - correctFeedbackVal.setSingleValue(QTI21Constants.CORRECT); + correctFeedbackVal.setSingleValue(QTI21Constants.CORRECT_IDENTIFIER_VALUE); correctFeedbackVar.setExpression(correctFeedbackVal); responseElseIf.getResponseRules().add(correctFeedbackVar); } @@ -276,7 +515,7 @@ public class AssessmentItemFactory { incorrectFeedbackVar.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); BaseValue incorrectFeedbackVal = new BaseValue(incorrectFeedbackVar); incorrectFeedbackVal.setBaseTypeAttrValue(BaseType.IDENTIFIER); - incorrectFeedbackVal.setSingleValue(QTI21Constants.INCORRECT); + incorrectFeedbackVal.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE); incorrectFeedbackVar.setExpression(incorrectFeedbackVal); responseElse.getResponseRules().add(incorrectFeedbackVar); } diff --git a/src/main/java/org/olat/ims/qti21/model/xml/ChoiceAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/ChoiceAssessmentItemBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..82303f8962146770e36a560b7d57756afaa77c24 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/ChoiceAssessmentItemBuilder.java @@ -0,0 +1,113 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.transform.stream.StreamResult; + +import org.olat.core.gui.render.StringOutput; + +import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleChoice; +import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; +import uk.ac.ed.ph.jqtiplus.types.Identifier; + +/** + * + * Initial date: 08.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public abstract class ChoiceAssessmentItemBuilder extends AssessmentItemBuilder { + + protected boolean shuffle; + protected String question; + protected List<SimpleChoice> choices; + protected Identifier responseIdentifier; + protected ChoiceInteraction choiceInteraction; + + public ChoiceAssessmentItemBuilder(AssessmentItem assessmentItem, QtiSerializer qtiSerializer) { + super(assessmentItem, qtiSerializer); + } + + @Override + public void extract() { + super.extract(); + extractChoiceInteraction(); + } + + private void extractChoiceInteraction() { + StringOutput sb = new StringOutput(); + List<Block> blocks = assessmentItem.getItemBody().getBlocks(); + for(Block block:blocks) { + if(block instanceof ChoiceInteraction) { + choiceInteraction = (ChoiceInteraction)block; + responseIdentifier = choiceInteraction.getResponseIdentifier(); + shuffle = choiceInteraction.getShuffle(); + break; + } else { + qtiSerializer.serializeJqtiObject(block, new StreamResult(sb)); + } + } + question = sb.toString(); + + choices = new ArrayList<>(); + if(choiceInteraction != null) { + choices.addAll(choiceInteraction.getSimpleChoices()); + } + } + + public ChoiceInteraction getChoiceInteraction() { + return choiceInteraction; + } + + public boolean isShuffle() { + return shuffle; + } + + public void setShuffle(boolean shuffle) { + this.shuffle = shuffle; + } + + /** + * Return the HTML block before the choice interaction as a string. + * + * @return + */ + public String getQuestion() { + return question; + } + + public void setQuestion(String html) { + this.question = html; + } + + public List<SimpleChoice> getSimpleChoices() { + return choices; + } + + public void setSimpleChoices(List<SimpleChoice> choices) { + this.choices = new ArrayList<>(choices); + } +} diff --git a/src/main/java/org/olat/ims/qti21/model/xml/ModalFeedbackBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/ModalFeedbackBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..7c5eb63f61203eb1b93b6c303428a49bec066b0b --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/ModalFeedbackBuilder.java @@ -0,0 +1,179 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import java.util.List; + +import org.olat.ims.qti21.QTI21Constants; + +import uk.ac.ed.ph.jqtiplus.attribute.value.StringAttribute; +import uk.ac.ed.ph.jqtiplus.node.expression.Expression; +import uk.ac.ed.ph.jqtiplus.node.expression.general.BaseValue; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.ModalFeedback; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseCondition; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseIf; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseRule; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.SetOutcomeValue; +import uk.ac.ed.ph.jqtiplus.types.Identifier; +import uk.ac.ed.ph.jqtiplus.value.IdentifierValue; +import uk.ac.ed.ph.jqtiplus.value.SingleValue; + +/** + * + * Initial date: 09.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ModalFeedbackBuilder { + + private final ModalFeedback modalFeedback; + private final AssessmentItem assessmentItem; + + private String title; + private String text; + private Identifier identifier; + + public ModalFeedbackBuilder(AssessmentItem assessmentItem, ModalFeedback modalFeedback) { + this.assessmentItem = assessmentItem; + this.modalFeedback = modalFeedback; + if(modalFeedback != null) { + text = new AssessmentBuilderHelper().toString(modalFeedback.getFlowStatics()); + StringAttribute titleAttr = modalFeedback.getAttributes().getStringAttribute(ModalFeedback.ATTR_TITLE_NAME); + title = titleAttr == null ? null : titleAttr.getComputedValue(); + identifier = modalFeedback.getIdentifier(); + + } + } + + public Identifier getModalFeedbackIdentifier() { + return modalFeedback.getIdentifier(); + } + + public ResponseRule getResponseRule() { + ResponseRule feedbackRule = findFeedbackRule(modalFeedback.getIdentifier()); + return feedbackRule; + } + + public boolean isCorrectRule() { + ResponseRule feedbackRule = findFeedbackRule(modalFeedback.getIdentifier()); + return findFeedbackRule(feedbackRule, QTI21Constants.CORRECT_IDENTIFIER); + } + + public boolean isIncorrectRule() { + ResponseRule feedbackRule = findFeedbackRule(modalFeedback.getIdentifier()); + return findFeedbackRule(feedbackRule, QTI21Constants.INCORRECT_IDENTIFIER); + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Identifier getIdentifier() { + return identifier; + } + + public void setIdentifier(Identifier identifier) { + this.identifier = identifier; + } + + private ResponseRule findFeedbackRule(Identifier feedbackIdentifier) { + List<ResponseRule> responseRules = assessmentItem.getResponseProcessing().getResponseRules(); + for(ResponseRule responseRule:responseRules) { + if(responseRule instanceof ResponseCondition) { + if(findFeedbackRuleInSetOutcomeVariable(responseRule, feedbackIdentifier)) { + return responseRule; + } + } + } + return null; + } + + private boolean findFeedbackRuleInSetOutcomeVariable(ResponseRule responseRule, Identifier feedbackIdentifier) { + if(responseRule instanceof ResponseCondition) { + ResponseCondition responseCondition = (ResponseCondition)responseRule; + ResponseIf responseIf = responseCondition.getResponseIf(); + List<ResponseRule> ifResponseRules = responseIf.getResponseRules(); + for(ResponseRule ifResponseRule:ifResponseRules) { + if(ifResponseRule instanceof SetOutcomeValue) { + SetOutcomeValue setOutcomeValue = (SetOutcomeValue)ifResponseRule; + if(findFeedbackRuleInExpression(setOutcomeValue.getExpression(), feedbackIdentifier)) { + return true; + } + } + } + } + return false; + } + + private boolean findFeedbackRule(ResponseRule responseRule, Identifier id) { + if(responseRule instanceof ResponseCondition) { + ResponseCondition responseCondition = (ResponseCondition)responseRule; + ResponseIf responseIf = responseCondition.getResponseIf(); + List<Expression> expressions = responseIf.getExpressions(); + if(findFeedbackRuleInExpression(expressions, id)) { + return true; + } + } + return false; + } + + private boolean findFeedbackRuleInExpression(List<Expression> expressions, Identifier feedbackIdentifier) { + for(Expression expression:expressions) { + if(findFeedbackRuleInExpression(expression, feedbackIdentifier)) { + return true; + } + } + return false; + } + + private boolean findFeedbackRuleInExpression(Expression expression, Identifier feedbackIdentifier) { + if(expression instanceof BaseValue) { + BaseValue bValue = (BaseValue)expression; + SingleValue sValue = bValue.getSingleValue(); + if(sValue instanceof IdentifierValue) { + IdentifierValue iValue = (IdentifierValue)sValue; + if(feedbackIdentifier.equals(iValue.identifierValue())) { + return true; + } + } + } else { + List<Expression> childExpressions = expression.getExpressions(); + for(Expression childExpression:childExpressions) { + if(findFeedbackRuleInExpression(childExpression, feedbackIdentifier)) { + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/org/olat/ims/qti21/model/xml/ScoreBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/ScoreBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..d440289bfd1363f76b7e21b71e8837d6ea5e4227 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/ScoreBuilder.java @@ -0,0 +1,58 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import uk.ac.ed.ph.jqtiplus.node.outcome.declaration.OutcomeDeclaration; + +/** + * + * Initial date: 10.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ScoreBuilder { + + private Double score; + private OutcomeDeclaration outcomeDeclaration; + + public ScoreBuilder(Double score, OutcomeDeclaration outcomeDeclaration) { + this.score = score; + this.outcomeDeclaration = outcomeDeclaration; + } + + public Double getScore() { + return score; + } + + public void setScore(Double score) { + this.score = score; + } + + public OutcomeDeclaration getOutcomeDeclaration() { + return outcomeDeclaration; + } + + public void setOutcomeDeclaration(OutcomeDeclaration outcomeDeclaration) { + this.outcomeDeclaration = outcomeDeclaration; + } + + + +} diff --git a/src/main/java/org/olat/ims/qti21/model/xml/SingleChoiceAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/SingleChoiceAssessmentItemBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..57381148a5a06607c095ed6b2b6a699e29c9ecad --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/SingleChoiceAssessmentItemBuilder.java @@ -0,0 +1,244 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import java.util.List; + +import org.olat.ims.qti21.QTI21Constants; + +import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; +import uk.ac.ed.ph.jqtiplus.node.expression.general.BaseValue; +import uk.ac.ed.ph.jqtiplus.node.expression.general.Correct; +import uk.ac.ed.ph.jqtiplus.node.expression.general.Variable; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.IsNull; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.Match; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.Sum; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleChoice; +import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseCondition; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElse; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElseIf; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseIf; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseRule; +import uk.ac.ed.ph.jqtiplus.node.item.response.processing.SetOutcomeValue; +import uk.ac.ed.ph.jqtiplus.node.shared.FieldValue; +import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; +import uk.ac.ed.ph.jqtiplus.types.ComplexReferenceIdentifier; +import uk.ac.ed.ph.jqtiplus.types.Identifier; +import uk.ac.ed.ph.jqtiplus.value.BaseType; +import uk.ac.ed.ph.jqtiplus.value.Cardinality; +import uk.ac.ed.ph.jqtiplus.value.IdentifierValue; +import uk.ac.ed.ph.jqtiplus.value.Value; + +/** + * + * Initial date: 08.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SingleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBuilder { + + private Identifier correctAnswer; + + public SingleChoiceAssessmentItemBuilder(AssessmentItem assessmentItem, QtiSerializer qtiSerializer) { + super(assessmentItem, qtiSerializer); + } + + @Override + public void extract() { + super.extract(); + + List<ResponseDeclaration> responseDeclarations = assessmentItem.getResponseDeclarations(); + if(responseDeclarations.size() == 1) { + CorrectResponse correctResponse = responseDeclarations.get(0).getCorrectResponse(); + if(correctResponse != null) { + List<FieldValue> values = correctResponse.getFieldValues(); + Value value = FieldValue.computeValue(Cardinality.SINGLE, values); + if(value instanceof IdentifierValue) { + IdentifierValue identifierValue = (IdentifierValue)value; + correctAnswer = identifierValue.identifierValue(); + } + } + } + } + + public boolean isCorrect(SimpleChoice choice) { + return correctAnswer != null && correctAnswer.equals(choice.getIdentifier()); + } + + public void setCorrectAnswer(Identifier identifier) { + correctAnswer = identifier; + } + + @Override + protected void buildResponseDeclaration() { + ResponseDeclaration responseDeclaration = AssessmentItemFactory + .createSingleChoiceCorrectResponseDeclaration(assessmentItem, responseIdentifier, correctAnswer); + assessmentItem.getResponseDeclarations().add(responseDeclaration); + } + + @Override + protected void buildItemBody() { + //remove current blocks + List<Block> blocks = assessmentItem.getItemBody().getBlocks(); + blocks.clear(); + + //add question + List<Block> questionBlocks = getHelper().parseHtml(question); + blocks.addAll(questionBlocks); + + //add interaction + ChoiceInteraction singleChoiceInteraction = AssessmentItemFactory + .createSingleChoiceInteraction(assessmentItem, responseIdentifier); + singleChoiceInteraction.setShuffle(isShuffle()); + blocks.add(singleChoiceInteraction); + List<SimpleChoice> choiceList = getSimpleChoices(); + singleChoiceInteraction.getSimpleChoices().addAll(choiceList); + } + + @Override + protected void buildMainScoreRule(List<ResponseRule> responseRules) { + ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing()); + responseRules.add(0, rule); + /* + <responseIf> + <isNull> + <variable identifier="RESPONSE_1" /> + </isNull> + <setOutcomeValue identifier="FEEDBACKBASIC"> + <baseValue baseType="identifier"> + empty + </baseValue> + </setOutcomeValue> + </responseIf> + */ + + //if no response + ResponseIf responseIf = new ResponseIf(rule); + rule.setResponseIf(responseIf); + + IsNull isNull = new IsNull(responseIf); + responseIf.getExpressions().add(isNull); + + Variable variable = new Variable(isNull); + variable.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier.toString())); + isNull.getExpressions().add(variable); + + { + SetOutcomeValue feedbackVar = new SetOutcomeValue(responseIf); + feedbackVar.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); + BaseValue feedbackVal = new BaseValue(feedbackVar); + feedbackVal.setBaseTypeAttrValue(BaseType.IDENTIFIER); + feedbackVal.setSingleValue(new IdentifierValue("empty")); + feedbackVar.setExpression(feedbackVal); + responseIf.getResponseRules().add(feedbackVar); + } + /* + <responseElseIf> + <match> + <variable identifier="RESPONSE_1" /><correct identifier="RESPONSE_1" /> + </match> + <setOutcomeValue identifier="SCORE"> + <sum> + <variable identifier="SCORE" /><variable identifier="MAXSCORE" /> + </sum> + </setOutcomeValue> + <setOutcomeValue identifier="FEEDBACKBASIC"> + <baseValue baseType="identifier"> + correct + </baseValue> + </setOutcomeValue> + </responseElseIf> + */ + + //else if correct response + ResponseElseIf responseElseIf = new ResponseElseIf(rule); + rule.getResponseElseIfs().add(responseElseIf); + + //match + { + Match match = new Match(responseElseIf); + responseElseIf.getExpressions().add(match); + + Variable responseVar = new Variable(match); + responseVar.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier.toString())); + match.getExpressions().add(responseVar); + + Correct correct = new Correct(match); + correct.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier.toString())); + match.getExpressions().add(correct); + } + + // outcome score + { + SetOutcomeValue scoreOutcomeVar = new SetOutcomeValue(responseIf); + scoreOutcomeVar.setIdentifier(QTI21Constants.SCORE_IDENTIFIER); + responseElseIf.getResponseRules().add(scoreOutcomeVar); + + Sum sum = new Sum(scoreOutcomeVar); + scoreOutcomeVar.getExpressions().add(sum); + + Variable scoreVar = new Variable(sum); + scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER); + sum.getExpressions().add(scoreVar); + + Variable maxScoreVar = new Variable(sum); + maxScoreVar.setIdentifier(QTI21Constants.MAXSCORE_CLX_IDENTIFIER); + sum.getExpressions().add(maxScoreVar); + } + + // outcome feedback + { + SetOutcomeValue correctFeedbackVar = new SetOutcomeValue(responseIf); + correctFeedbackVar.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); + BaseValue correctFeedbackVal = new BaseValue(correctFeedbackVar); + correctFeedbackVal.setBaseTypeAttrValue(BaseType.IDENTIFIER); + correctFeedbackVal.setSingleValue(QTI21Constants.CORRECT_IDENTIFIER_VALUE); + correctFeedbackVar.setExpression(correctFeedbackVal); + responseElseIf.getResponseRules().add(correctFeedbackVar); + } + + /* + <responseElse> + <setOutcomeValue identifier="FEEDBACKBASIC"> + <baseValue baseType="identifier"> + incorrect + </baseValue> + </setOutcomeValue> + </responseElse> + </responseCondition> + */ + + ResponseElse responseElse = new ResponseElse(rule); + rule.setResponseElse(responseElse); + {// feedback incorrect + SetOutcomeValue incorrectFeedbackVar = new SetOutcomeValue(responseIf); + incorrectFeedbackVar.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); + BaseValue incorrectFeedbackVal = new BaseValue(incorrectFeedbackVar); + incorrectFeedbackVal.setBaseTypeAttrValue(BaseType.IDENTIFIER); + incorrectFeedbackVal.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE); + incorrectFeedbackVar.setExpression(incorrectFeedbackVal); + responseElse.getResponseRules().add(incorrectFeedbackVar); + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java index ffb53bfd9ef3d6eb4638ff34d9de7a4a9448ecfd..76d55a501c976dee9a324f60d5f3f9ece89dc9d8 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java @@ -253,9 +253,10 @@ public class AssessmentObjectVelocityRenderDecorator extends VelocityRenderDecor choices = interaction.getSimpleChoices(); } - return choices.stream() + List<SimpleChoice> visibleChoices = choices.stream() .filter((choice) -> isVisible(choice, itemSessionState)) .collect(Collectors.toList()); + return visibleChoices; } public List<SimpleChoice> getVisibleOrderedSimpleChoices(OrderInteraction interaction) { diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java index 4db2bab27e4e5d804b288b8a4e6ad98fb863f3b1..1104d95de6ea174d29e02565cccb92913ce42488 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java @@ -25,7 +25,6 @@ import java.util.List; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; -import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.tabbedpane.TabbedPane; import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.Controller; @@ -34,6 +33,8 @@ import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.BasicController; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.QTI21Service; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.SingleChoiceAssessmentItemBuilder; import org.olat.ims.qti21.ui.AssessmentItemDisplayController; import org.olat.modules.assessment.AssessmentEntry; import org.olat.modules.assessment.AssessmentService; @@ -59,7 +60,7 @@ public class AssessmentItemEditorController extends BasicController { private final TabbedPane tabbedPane; private final VelocityContainer mainVC; - private FormBasicController itemEditor; + private EditorController itemEditor, scoreEditor, feedbackEditor; private AssessmentItemDisplayController displayCtrl; @Autowired @@ -113,9 +114,7 @@ public class AssessmentItemEditorController extends BasicController { } if(choice && !unkown) { - itemEditor = new SingleChoiceEditorController(ureq, getWindowControl()); - listenTo(itemEditor); - tabbedPane.addTab("Choice", itemEditor.getInitialComponent()); + initSingleChoiceEditors(ureq, item); } else if(unkown) { initItemCreatedByUnkownEditor(ureq); } @@ -129,6 +128,20 @@ public class AssessmentItemEditorController extends BasicController { listenTo(itemEditor); tabbedPane.addTab("Unkown", itemEditor.getInitialComponent()); } + + private void initSingleChoiceEditors(UserRequest ureq, AssessmentItem item) { + SingleChoiceAssessmentItemBuilder itemBuilder = new SingleChoiceAssessmentItemBuilder(item, qtiService.qtiSerializer()); + itemEditor = new SingleChoiceEditorController(ureq, getWindowControl(), itemBuilder); + listenTo(itemEditor); + scoreEditor = new SingleChoiceScoreController(ureq, getWindowControl(), itemBuilder); + listenTo(scoreEditor); + feedbackEditor = new FeedbackEditorController(ureq, getWindowControl(), itemBuilder); + listenTo(feedbackEditor); + + tabbedPane.addTab("Choice", itemEditor.getInitialComponent()); + tabbedPane.addTab("Score", scoreEditor.getInitialComponent()); + tabbedPane.addTab("Feedback", feedbackEditor.getInitialComponent()); + } @Override protected void event(UserRequest ureq, Component source, Event event) { @@ -138,10 +151,16 @@ public class AssessmentItemEditorController extends BasicController { @Override protected void event(UserRequest ureq, Controller source, Event event) { if(event instanceof AssessmentItemEvent) { - if(event == AssessmentItemEvent.CHANGED_EVENT) { + if(event == AssessmentItemEvent.ASSESSMENT_ITEM_CHANGED) { + if(source instanceof EditorController) { + EditorController editorCtrl = (EditorController)source; + AssessmentItemBuilder builder = editorCtrl.getBuilder(); + if(builder != null) { + builder.build(); + } + } doSaveAssessmentItem(); } - } super.event(ureq, source, event); } diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/EditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/EditorController.java new file mode 100644 index 0000000000000000000000000000000000000000..0f761f189ecff1cc130a6207dc98d9c37126d2ca --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/EditorController.java @@ -0,0 +1,31 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.ui.editor; + +import org.olat.core.gui.control.Controller; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; + +public interface EditorController extends Controller { + + public void updateFromBuilder(); + + public AssessmentItemBuilder getBuilder(); + +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/FeedbackEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/FeedbackEditorController.java new file mode 100644 index 0000000000000000000000000000000000000000..5673318c3af825627b6c3499914d90a5c7d069fb --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/FeedbackEditorController.java @@ -0,0 +1,125 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.ui.editor; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.RichTextElement; +import org.olat.core.gui.components.form.flexible.elements.TextElement; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; +import org.olat.core.gui.components.form.flexible.impl.elements.richText.RichTextConfiguration; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.util.StringHelper; +import org.olat.core.util.filter.FilterFactory; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.ModalFeedbackBuilder; +import org.olat.ims.qti21.model.xml.SingleChoiceAssessmentItemBuilder; + +/** + * + * Initial date: 09.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class FeedbackEditorController extends FormBasicController implements EditorController { + + private TextElement feedbackCorrectTitleEl, feedbackIncorrectTitleEl; + private RichTextElement feedbackCorrectTextEl, feedbackIncorrectTextEl; + + private SingleChoiceAssessmentItemBuilder itemBuilder; + + public FeedbackEditorController(UserRequest ureq, WindowControl wControl, SingleChoiceAssessmentItemBuilder itemBuilder) { + super(ureq, wControl); + this.itemBuilder = itemBuilder; + initForm(ureq); + } + + @Override + public void updateFromBuilder() { + // + } + + @Override + public AssessmentItemBuilder getBuilder() { + return itemBuilder; + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + //correct feedback + ModalFeedbackBuilder correctFeedback = itemBuilder.getCorrectFeedback(); + String correctTitle = correctFeedback == null ? "" : correctFeedback.getTitle(); + feedbackCorrectTitleEl = uifactory.addTextElement("correctTitle", "form.imd.correct.title", -1, correctTitle, formLayout); + feedbackCorrectTitleEl.setUserObject(correctFeedback); + String correctText = correctFeedback == null ? "" : correctFeedback.getText(); + feedbackCorrectTextEl = uifactory.addRichTextElementForStringData("correctText", "form.imd.correct.text", correctText, 8, -1, true, null, null, + formLayout, ureq.getUserSession(), getWindowControl()); + RichTextConfiguration richTextConfig = feedbackCorrectTextEl.getEditorConfiguration(); + richTextConfig.setFileBrowserUploadRelPath("media");// set upload dir to the media dir + + //incorrect feedback + ModalFeedbackBuilder incorrectFeedback = itemBuilder.getIncorrectFeedback(); + String incorrectTitle = incorrectFeedback == null ? "" : incorrectFeedback.getTitle(); + feedbackIncorrectTitleEl = uifactory.addTextElement("incorrectTitle", "form.imd.incorrect.title", -1, incorrectTitle, formLayout); + feedbackIncorrectTitleEl.setUserObject(incorrectFeedback); + String incorrectText = incorrectFeedback == null ? "" : incorrectFeedback.getText(); + feedbackIncorrectTextEl = uifactory.addRichTextElementForStringData("incorrectText", "form.imd.incorrect.text", incorrectText, 8, -1, true, null, null, + formLayout, ureq.getUserSession(), getWindowControl()); + RichTextConfiguration richTextConfig2 = feedbackIncorrectTextEl.getEditorConfiguration(); + richTextConfig2.setFileBrowserUploadRelPath("media");// set upload dir to the media dir + + // Submit Button + FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); + buttonsContainer.setRootForm(mainForm); + formLayout.add(buttonsContainer); + uifactory.addFormSubmitButton("submit", buttonsContainer); + } + + @Override + protected void formOK(UserRequest ureq) { + String correctTitle = feedbackCorrectTitleEl.getValue(); + String correctText = feedbackCorrectTextEl.getValue(); + if(StringHelper.containsNonWhitespace(FilterFactory.getHtmlTagsFilter().filter(correctText))) { + ModalFeedbackBuilder correctBuilder = itemBuilder.getCorrectFeedback(); + if(correctBuilder == null) { + correctBuilder = itemBuilder.createCorrectFeedback(); + } + correctBuilder.setTitle(correctTitle); + correctBuilder.setText(correctText); + } + + String incorrectTitle = feedbackIncorrectTitleEl.getValue(); + String incorrectText = feedbackIncorrectTextEl.getValue(); + if(StringHelper.containsNonWhitespace(correctTitle)) { + ModalFeedbackBuilder incorrectBuilder = (ModalFeedbackBuilder)feedbackIncorrectTitleEl.getUserObject(); + incorrectBuilder.setTitle(incorrectTitle); + incorrectBuilder.setText(incorrectText); + } + + fireEvent(ureq, AssessmentItemEvent.ASSESSMENT_ITEM_CHANGED); + } + + @Override + protected void doDispose() { + // + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/SingleChoiceEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/SingleChoiceEditorController.java index 05f848c5b2e7644412bbcde92c176c757e9ea279..c35cdadc940afc84f83ed0a129fc0948cc190ecf 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/SingleChoiceEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/SingleChoiceEditorController.java @@ -19,11 +19,35 @@ */ package org.olat.ims.qti21.ui.editor; +import java.util.ArrayList; +import java.util.List; + import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.FormLink; +import org.olat.core.gui.components.form.flexible.elements.RichTextElement; +import org.olat.core.gui.components.form.flexible.elements.SingleSelection; +import org.olat.core.gui.components.form.flexible.elements.TextElement; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormEvent; +import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; +import org.olat.core.gui.components.form.flexible.impl.elements.richText.RichTextConfiguration; +import org.olat.core.gui.components.link.Link; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; +import org.olat.core.util.StringHelper; +import org.olat.ims.qti21.model.IdentifierGenerator; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.AssessmentItemFactory; +import org.olat.ims.qti21.model.xml.SingleChoiceAssessmentItemBuilder; + +import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; +import uk.ac.ed.ph.jqtiplus.node.content.basic.FlowStatic; +import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.P; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleChoice; +import uk.ac.ed.ph.jqtiplus.types.Identifier; /** * @@ -31,27 +55,306 @@ import org.olat.core.gui.control.WindowControl; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -public class SingleChoiceEditorController extends FormBasicController { +public class SingleChoiceEditorController extends FormBasicController implements EditorController { + + private TextElement titleEl; + private RichTextElement textEl; + private FormLayoutContainer answersCont; + private final List<SimpleChoiceWrapper> choiceWrappers = new ArrayList<>(); - public SingleChoiceEditorController(UserRequest ureq, WindowControl wControl) { - super(ureq, wControl); - + private int count = 0; + private final SingleChoiceAssessmentItemBuilder itemBuilder; + + public SingleChoiceEditorController(UserRequest ureq, WindowControl wControl, SingleChoiceAssessmentItemBuilder itemBuilder) { + super(ureq, wControl, "simple_choices_editor"); + this.itemBuilder = itemBuilder; initForm(ureq); } + + private SingleSelection shuffleEl; + + private static final String[] yesnoKeys = new String[]{ "y", "n"}; @Override protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { setFormTitle("editor.sc.title"); - // + + FormLayoutContainer metadata = FormLayoutContainer.createDefaultFormLayout("metadata", getTranslator()); + metadata.setRootForm(mainForm); + formLayout.add(metadata); + formLayout.add("metadata", metadata); + + titleEl = uifactory.addTextElement("title", "form.imd.title", -1, itemBuilder.getTitle(), metadata); + titleEl.setMandatory(true); + + String description = itemBuilder.getQuestion(); + textEl = uifactory.addRichTextElementForStringData("desc", "form.imd.descr", description, 8, -1, true, null, null, + metadata, ureq.getUserSession(), getWindowControl()); + RichTextConfiguration richTextConfig = textEl.getEditorConfiguration(); + richTextConfig.setFileBrowserUploadRelPath("media");// set upload dir to the media dir + + //points -> in other controller + + //shuffle + String[] yesnoValues = new String[]{ translate("yes"), translate("no") }; + shuffleEl = uifactory.addRadiosHorizontal("shuffle", "form.imd.shuffle", formLayout, yesnoKeys, yesnoValues); + if (itemBuilder.isShuffle()) { + shuffleEl.select("y", true); + } else { + shuffleEl.select("n", true); + } + + //responses + String page = velocity_root + "/simple_choices.html"; + answersCont = FormLayoutContainer.createCustomFormLayout("answers", getTranslator(), page); + answersCont.setRootForm(mainForm); + formLayout.add(answersCont); + formLayout.add("answers", answersCont); + + ChoiceInteraction interaction = itemBuilder.getChoiceInteraction(); + if(interaction != null) { + + List<SimpleChoice> choices = itemBuilder.getSimpleChoices(); + for(SimpleChoice choice:choices) { + wrapAnswer(ureq, choice); + } + } + answersCont.contextPut("choices", choiceWrappers); + recalculateUpDownLinks(); + + // Submit Button + FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); + buttonsContainer.setRootForm(mainForm); + formLayout.add(buttonsContainer); + formLayout.add("buttons", buttonsContainer); + uifactory.addFormSubmitButton("submit", buttonsContainer); + } + + @Override + public void updateFromBuilder() { + } + @Override + public AssessmentItemBuilder getBuilder() { + return itemBuilder; + } + + private void wrapAnswer(UserRequest ureq, SimpleChoice choice) { + String choiceContent = itemBuilder.getHelper().toString(choice.getFlowStatics()); + String choiceId = "answer" + count++; + RichTextElement choiceEl = uifactory.addRichTextElementForStringData(choiceId, "form.imd.answer", choiceContent, 8, -1, true, null, null, + answersCont, ureq.getUserSession(), getWindowControl()); + choiceEl.setUserObject(choice); + answersCont.add("choiceId", choiceEl); + + FormLink removeLink = uifactory.addFormLink("rm-".concat(choiceId), "rm", "", null, answersCont, Link.NONTRANSLATED); + removeLink.setIconLeftCSS("o_icon o_icon-lg o_icon_delete"); + answersCont.add(removeLink); + answersCont.add("rm-".concat(choiceId), removeLink); + + FormLink addLink = uifactory.addFormLink("add-".concat(choiceId), "add", "", null, answersCont, Link.NONTRANSLATED); + addLink.setIconLeftCSS("o_icon o_icon-lg o_icon_add"); + answersCont.add(addLink); + answersCont.add("add-".concat(choiceId), addLink); + + FormLink upLink = uifactory.addFormLink("up-".concat(choiceId), "up", "", null, answersCont, Link.NONTRANSLATED); + upLink.setIconLeftCSS("o_icon o_icon-lg o_icon_move_up"); + answersCont.add(upLink); + answersCont.add("up-".concat(choiceId), upLink); + + FormLink downLink = uifactory.addFormLink("down-".concat(choiceId), "down", "", null, answersCont, Link.NONTRANSLATED); + downLink.setIconLeftCSS("o_icon o_icon-lg o_icon_move_down"); + answersCont.add(downLink); + answersCont.add("down-".concat(choiceId), downLink); + + boolean correct = itemBuilder.isCorrect(choice); + choiceWrappers.add(new SimpleChoiceWrapper(choice, correct, choiceEl, removeLink, addLink, upLink, downLink)); + } + @Override protected void doDispose() { // } + @Override + protected boolean validateFormLogic(UserRequest ureq) { + boolean allOk = true; + + titleEl.clearError(); + if(!StringHelper.containsNonWhitespace(titleEl.getValue())) { + titleEl.setErrorKey("form.legende.mandatory", null); + allOk &= false; + } + + return allOk & super.validateFormLogic(ureq); + } + @Override protected void formOK(UserRequest ureq) { - // + //title + itemBuilder.setTitle(titleEl.getValue()); + //question + String questionText = textEl.getValue(); + itemBuilder.setQuestion(questionText); + + //correct response + String correctAnswer = ureq.getParameter("correct"); + Identifier correctAnswerIdentifier = Identifier.parseString(correctAnswer); + itemBuilder.setCorrectAnswer(correctAnswerIdentifier); + + //shuffle + itemBuilder.setShuffle(shuffleEl.isOneSelected() && shuffleEl.isSelected(0)); + + //replace simple choices + List<SimpleChoice> choiceList = new ArrayList<>(); + for(SimpleChoiceWrapper choiceWrapper:choiceWrappers) { + SimpleChoice choice = choiceWrapper.getSimpleChoice(); + choiceWrapper.setCorrect(correctAnswerIdentifier.equals(choiceWrapper.getIdentifier())); + //text + String answer = choiceWrapper.getAnswer().getValue(); + List<Block> blocks = itemBuilder.getHelper().parseHtml(answer); + choice.getFlowStatics().clear(); + for(Block block:blocks) { + if(block instanceof FlowStatic) { + choice.getFlowStatics().add((FlowStatic)block); + } + } + + choiceList.add(choice); + } + itemBuilder.setSimpleChoices(choiceList); + + fireEvent(ureq, AssessmentItemEvent.ASSESSMENT_ITEM_CHANGED); + } + + @Override + protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { + if(source instanceof FormLink) { + FormLink button = (FormLink)source; + String cmd = button.getCmd(); + if("rm".equals(cmd)) { + doRemoveSimpleChoice((SimpleChoiceWrapper)button.getUserObject()); + } else if("add".equals(cmd)) { + doAddSimpleChoice(ureq); + } else if("up".equals(cmd)) { + doMoveSimpleChoiceUp((SimpleChoiceWrapper)button.getUserObject()); + } else if("down".equals(cmd)) { + doMoveSimpleChoiceDown((SimpleChoiceWrapper)button.getUserObject()); + } + } + super.formInnerEvent(ureq, source, event); + } + + private void doAddSimpleChoice(UserRequest ureq) { + ChoiceInteraction interaction = itemBuilder.getChoiceInteraction(); + SimpleChoice newChoice = new SimpleChoice(interaction); + newChoice.setIdentifier(IdentifierGenerator.newAsIdentifier()); + P firstChoiceText = AssessmentItemFactory.getParagraph(newChoice, "New answer"); + newChoice.getFlowStatics().add(firstChoiceText); + + wrapAnswer(ureq, newChoice); + flc.setDirty(true); + } + + private void doRemoveSimpleChoice(SimpleChoiceWrapper choiceWrapper) { + choiceWrappers.remove(choiceWrapper); + flc.setDirty(true); + } + + private void doMoveSimpleChoiceUp(SimpleChoiceWrapper choiceWrapper) { + int index = choiceWrappers.indexOf(choiceWrapper) - 1; + if(index >= 0 && index < choiceWrappers.size()) { + choiceWrappers.remove(choiceWrapper); + choiceWrappers.add(index, choiceWrapper); + } + recalculateUpDownLinks(); + flc.setDirty(true); + } + + private void doMoveSimpleChoiceDown(SimpleChoiceWrapper choiceWrapper) { + int index = choiceWrappers.indexOf(choiceWrapper) + 1; + if(index > 0 && index < choiceWrappers.size()) { + choiceWrappers.remove(choiceWrapper); + choiceWrappers.add(index, choiceWrapper); + } + recalculateUpDownLinks(); + flc.setDirty(true); + } + + private void recalculateUpDownLinks() { + int numOfChoices = choiceWrappers.size(); + for(int i=0; i<numOfChoices; i++) { + SimpleChoiceWrapper choiceWrapper = choiceWrappers.get(i); + choiceWrapper.getUp().setEnabled(i != 0); + choiceWrapper.getDown().setEnabled(i < (numOfChoices - 1)); + } + } + + public static final class SimpleChoiceWrapper { + + private final SimpleChoice choice; + private final RichTextElement answerEl; + private final FormLink removeLink, addLink, upLink, downLink; + + private boolean correct; + private final Identifier choiceIdentifier; + + public SimpleChoiceWrapper(SimpleChoice choice, boolean correct, RichTextElement answerEl, + FormLink removeLink, FormLink addLink, FormLink upLink, FormLink downLink) { + this.choice = choice; + this.correct = correct; + this.choiceIdentifier = choice.getIdentifier(); + this.answerEl = answerEl; + answerEl.setUserObject(this); + this.removeLink = removeLink; + removeLink.setUserObject(this); + this.addLink = addLink; + addLink.setUserObject(this); + this.upLink = upLink; + upLink.setUserObject(this); + this.downLink = downLink; + downLink.setUserObject(this); + } + + public Identifier getIdentifier() { + return choiceIdentifier; + } + + public String getIdentifierString() { + return choiceIdentifier.toString(); + } + + public boolean isCorrect() { + return correct; + } + + public void setCorrect(boolean correct) { + this.correct = correct; + } + + public SimpleChoice getSimpleChoice() { + return choice; + } + + public RichTextElement getAnswer() { + return answerEl; + } + + public FormLink getRemove() { + return removeLink; + } + + public FormLink getAdd() { + return addLink; + } + + public FormLink getUp() { + return upLink; + } + + public FormLink getDown() { + return downLink; + } } } diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/SingleChoiceScoreController.java b/src/main/java/org/olat/ims/qti21/ui/editor/SingleChoiceScoreController.java new file mode 100644 index 0000000000000000000000000000000000000000..9c47e8a1582dc7000758476f8e34fdb4891f558e --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/SingleChoiceScoreController.java @@ -0,0 +1,93 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.ui.editor; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.TextElement; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.ScoreBuilder; +import org.olat.ims.qti21.model.xml.SingleChoiceAssessmentItemBuilder; + +/** + * + * Initial date: 08.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SingleChoiceScoreController extends FormBasicController implements EditorController { + + private TextElement minScoreEl; + private TextElement maxScoreEl; + + private SingleChoiceAssessmentItemBuilder itemBuilder; + + public SingleChoiceScoreController(UserRequest ureq, WindowControl wControl, SingleChoiceAssessmentItemBuilder itemBuilder) { + super(ureq, wControl); + this.itemBuilder = itemBuilder; + initForm(ureq); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + minScoreEl = uifactory.addTextElement("min.score", "min.score", 8, "0.0", formLayout); + minScoreEl.setEnabled(false); + + ScoreBuilder maxScore = itemBuilder.getMaxScoreBuilder(); + String maxValue = maxScore == null ? "" : (maxScore.getScore() == null ? "" : maxScore.getScore().toString()); + maxScoreEl = uifactory.addTextElement("max.score", "max.score", 8, maxValue, formLayout); + + // Submit Button + FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); + buttonsContainer.setRootForm(mainForm); + formLayout.add(buttonsContainer); + uifactory.addFormSubmitButton("submit", buttonsContainer); + } + + @Override + public void updateFromBuilder() { + + } + + @Override + public AssessmentItemBuilder getBuilder() { + return itemBuilder; + } + + @Override + protected void formOK(UserRequest ureq) { + String maxScoreValue = maxScoreEl.getValue(); + Double maxScore = Double.parseDouble(maxScoreValue); + itemBuilder.setMaxScore(maxScore); + itemBuilder.setMinScore(new Double(0d)); + } + + @Override + protected void doDispose() { + // + } + + + +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/UnkownItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/UnkownItemEditorController.java index c44c0bf8cea77c6d3b4f5cb876f1d511537a16ae..9184c7a241e0905cb1d9d5b523ab80a18dd223a4 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/UnkownItemEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/UnkownItemEditorController.java @@ -24,6 +24,7 @@ import org.olat.core.gui.components.form.flexible.FormItemContainer; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; /** * @@ -31,7 +32,7 @@ import org.olat.core.gui.control.WindowControl; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -public class UnkownItemEditorController extends FormBasicController { +public class UnkownItemEditorController extends FormBasicController implements EditorController { public UnkownItemEditorController(UserRequest ureq, WindowControl wControl) { super(ureq, wControl); @@ -45,6 +46,16 @@ public class UnkownItemEditorController extends FormBasicController { // } + @Override + public void updateFromBuilder() { + // + } + + @Override + public AssessmentItemBuilder getBuilder() { + return null; + } + @Override protected void doDispose() { // diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_content/simple_choices.html b/src/main/java/org/olat/ims/qti21/ui/editor/_content/simple_choices.html new file mode 100644 index 0000000000000000000000000000000000000000..73e244176f545d4a8ff6674eefb6e7ad7b72e833 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_content/simple_choices.html @@ -0,0 +1,44 @@ +<fieldset class="o_form"> + +<div class="form-group #if($f.hasError($item)) has-feedback has-error #end clearfix"> + <div class="col-sm-2"><strong>[Correct]</strong></div> + <div class="col-sm-8"><strong>[Answer]</strong></div> + <div class="col-sm-2"><strong>[Actions]</strong></div> +</div> + +#foreach($choice in $choices) + +<div class="form-group #if($f.hasError($item)) has-feedback has-error #end clearfix"> + <div class="col-sm-2"> + <input type="radio" name="correct" value="${choice.getIdentifierString()}" #if(${choice.isCorrect()}) checked #end/> + </div> + <div class="col-sm-8"> + $r.render(${choice.getAnswer().getComponent().getComponentName()}) + #if($f.hasError($item)) + <span class="o_icon o_icon_error form-control-feedback"></span> + #end + </div> + <div class="col-sm-2"> + #if($r.available(${choice.getRemove().getComponent().getComponentName()}) && $r.visible(${choice.getRemove().getComponent().getComponentName()})) + $r.render(${choice.getRemove().getComponent().getComponentName()}) + #end + #if($r.available(${choice.getAdd().getComponent().getComponentName()}) && $r.visible(${choice.getAdd().getComponent().getComponentName()})) + $r.render(${choice.getAdd().getComponent().getComponentName()}) + #end + #if($r.available(${choice.getUp().getComponent().getComponentName()}) && $r.visible(${choice.getUp().getComponent().getComponentName()})) + $r.render(${choice.getUp().getComponent().getComponentName()}) + #end + #if($r.available(${choice.getDown().getComponent().getComponentName()}) && $r.visible(${choice.getDown().getComponent().getComponentName()})) + $r.render(${choice.getDown().getComponent().getComponentName()}) + #end + </div> + + #if($f.hasError($item)) + <div class="col-sm-offset-2 col-sm-8"> + $r.render("${item}_ERROR") + </div> + #end +</div> + +#end +</fieldset> \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_content/simple_choices_editor.html b/src/main/java/org/olat/ims/qti21/ui/editor/_content/simple_choices_editor.html new file mode 100644 index 0000000000000000000000000000000000000000..5245149a4f1b36645b6faa6ae16825dba831d807 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_content/simple_choices_editor.html @@ -0,0 +1,3 @@ +$r.render("metadata") +$r.render("answers") +$r.render("buttons") \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties index fc8aa54fd70ef73f3105a9ec0cc39e51a2382441..88cd94609c457d55e0bd694399d70ada03acb762 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties @@ -1,3 +1 @@ #Mon Mar 02 09:54:04 CET 2009 -editor.sc.title=Single choice -editor.unkown.title=Unkown interaction \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties index d186f30e90630ec062d32dfc5d60d44720643075..f38be912ca9aa039e8a94ee58c28d87bc7117c4d 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties @@ -1,3 +1,14 @@ #Sat Jan 22 17:01:28 CET 2011 editor.sc.title=Single choice -editor.unkown.title=Unkown interaction \ No newline at end of file +editor.unkown.title=Unkown interaction +form.imd.title=Titel +form.imd.descr=Frage +form.imd.answer=Antwort +form.imd.layout.horizontal=Horizontal +form.imd.layout.vertical=Vertical +form.imd.correct.title=Correct title +form.imd.incorrect.title=Incorrect title +form.imd.correct.text=Correct feedback +form.imd.incorrect.text=Incorrect feedback +min.score=Min. score +max.score=Max. score \ No newline at end of file diff --git a/src/main/java/org/olat/upgrade/OLATUpgrade_11_0_0.java b/src/main/java/org/olat/upgrade/OLATUpgrade_11_0_0.java index a93eafac80fbd96ce1fe9785d8fc4c3a4213d090..10002985fe28b9baffa36b33fa63f78b39683603 100644 --- a/src/main/java/org/olat/upgrade/OLATUpgrade_11_0_0.java +++ b/src/main/java/org/olat/upgrade/OLATUpgrade_11_0_0.java @@ -298,7 +298,7 @@ public class OLATUpgrade_11_0_0 extends OLATUpgrade { int nodeIdentIndex = propertyCategory.indexOf("::"); if(nodeIdentIndex > 0) { String nodeIdent = propertyCategory.substring(propertyCategory.indexOf("::") + 2); - AssessmentDataKey key = new AssessmentDataKey(property.getIdentity(), property.getResourceTypeId(), nodeIdent); + AssessmentDataKey key = new AssessmentDataKey(property.getIdentity().getKey(), property.getResourceTypeId(), nodeIdent); if(curentNodeAssessmentMap.containsKey(key)) { continue; } diff --git a/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2889dd2c80aa7ddc2638f699f1ffe4c0a6b17b39 --- /dev/null +++ b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java @@ -0,0 +1,67 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; + +import org.junit.Assert; +import org.junit.Test; +import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator; + +import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.reading.AssessmentObjectXmlLoader; +import uk.ac.ed.ph.jqtiplus.reading.QtiXmlReader; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; +import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; +import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator; + +public class AssessmentItemBuilderTest { + + private final JqtiExtensionManager jqtiExtensionManager = new JqtiExtensionManager(); + private final QtiXmlReader qtiXmlReader = new QtiXmlReader(jqtiExtensionManager); + private final QtiSerializer qtiSerializer = new QtiSerializer(jqtiExtensionManager); + + @Test + public void findFeedbacks() throws URISyntaxException { + URL itemUrl = AssessmentItemPackageTest.class.getResource("assessment-item-single-choice-feedbacks.xml"); + AssessmentItem assessmentItem = loadAssessmentItem(itemUrl); + SingleChoiceAssessmentItemBuilder itemBuilder = new SingleChoiceAssessmentItemBuilder(assessmentItem, qtiSerializer); + + ModalFeedbackBuilder correctFeedback = itemBuilder.getCorrectFeedback(); + Assert.assertNotNull(correctFeedback); + Assert.assertTrue(correctFeedback.isCorrectRule()); + + ModalFeedbackBuilder incorrectFeedback = itemBuilder.getIncorrectFeedback(); + Assert.assertNotNull(incorrectFeedback); + Assert.assertTrue(incorrectFeedback.isIncorrectRule()); + + } + + private AssessmentItem loadAssessmentItem(URL itemUrl) throws URISyntaxException { + ResourceLocator fileResourceLocator = new PathResourceLocator(Paths.get(itemUrl.toURI())); + AssessmentObjectXmlLoader assessmentObjectXmlLoader = new AssessmentObjectXmlLoader(qtiXmlReader, fileResourceLocator); + ResolvedAssessmentItem item = assessmentObjectXmlLoader.loadAndResolveAssessmentItem(itemUrl.toURI()); + return item.getItemLookup().getRootNodeHolder().getRootNode(); + } + +} diff --git a/src/test/java/org/olat/ims/qti21/model/xml/ConvertHTMLTestTest.java b/src/test/java/org/olat/ims/qti21/model/xml/ConvertHTMLTestTest.java new file mode 100644 index 0000000000000000000000000000000000000000..15138d0b51c7543e7756ba905f5e28eb4a8a2f6b --- /dev/null +++ b/src/test/java/org/olat/ims/qti21/model/xml/ConvertHTMLTestTest.java @@ -0,0 +1,138 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.model.xml; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.cyberneko.html.parsers.DOMParser; +import org.jcodec.common.Assert; +import org.junit.Test; +import org.olat.core.util.filter.FilterFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; +import uk.ac.ed.ph.jqtiplus.exception.QtiModelException; +import uk.ac.ed.ph.jqtiplus.node.LoadingContext; +import uk.ac.ed.ph.jqtiplus.node.QtiNode; +import uk.ac.ed.ph.jqtiplus.node.content.ItemBody; +import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.P; + +/** + * + * Initial date: 07.12.2015<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ConvertHTMLTestTest { + + @Test + public void convert() { + String content = "<html><p>Test</p><p>Test 2</p></html>"; + Document partialDocument = getDoc(content); + Element rootElement = partialDocument.getDocumentElement(); + + + + MyLoadingContext context = new MyLoadingContext(); + + Element paragraphEl = (Element)rootElement.getFirstChild(); + String tagName = paragraphEl.getTagName(); + String localName = paragraphEl.getLocalName(); + + ItemBody itemBody = new ItemBody(null); + itemBody.load(rootElement, context); + + Assert.assertNotNull(localName); + Assert.assertEquals(2, itemBody.getBlocks().size()); + + + + } + + private Document getDoc(String content) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new ByteArrayInputStream(content.getBytes())); + return doc; + } catch (ParserConfigurationException | SAXException | IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } + } + + private Document getDocument(String content) { + try { + DOMParser parser = new DOMParser(); + parser.setFeature("http://xml.org/sax/features/validation", false); + + parser.setFeature( "http://cyberneko.org/html/features/override-namespaces", true); + parser.setFeature ( "http://xml.org/sax/features/namespaces", true ); + parser.setProperty("http://cyberneko.org/html/properties/names/elems", "upper" ); // has no effect, cannot override xerces configuration + parser.setProperty( "http://cyberneko.org/html/properties/names/attrs", "upper" ); // has no effect, cannot override xerces configuration + parser.setFeature("http://cyberneko.org/html/features/balance-tags/document-fragment",true); + parser.parse(new InputSource(new StringReader(content))); + return parser.getDocument(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + + private List<P> getText(QtiNode choice, String htmlContent) { + String text = FilterFactory.getHtmlTagsFilter().filter(htmlContent); + P firstChoiceText = AssessmentItemFactory.getParagraph(choice, text); + List<P> blocks = new ArrayList<>(); + blocks.add(firstChoiceText); + return blocks; + } + + private static final class MyLoadingContext implements LoadingContext { + + @Override + public JqtiExtensionManager getJqtiExtensionManager() { + // + return null; + } + + @Override + public void modelBuildingError(QtiModelException exception, Node badNode) { + // + } + + } + +} diff --git a/src/test/java/org/olat/ims/qti21/model/xml/assessment-item-single-choice-feedbacks.xml b/src/test/java/org/olat/ims/qti21/model/xml/assessment-item-single-choice-feedbacks.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e93179da0b41368f554fdceaa151b176a5a4d3d --- /dev/null +++ b/src/test/java/org/olat/ims/qti21/model/xml/assessment-item-single-choice-feedbacks.xml @@ -0,0 +1,228 @@ +<?xml version="1.0" encoding="UTF-8"?> +<assessmentItem xmlns="http://www.imsglobal.org/xsd/imsqti_v2p1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/qti/qtiv2p1/imsqti_v2p1p1.xsd http://www.w3.org/1998/Math/MathML http://www.w3.org/Math/XMLSchema/mathml2/mathml2.xsd" identifier="id19a005ee-9a8c-4335-a0dd-adca2f8b97b4" title="New question" adaptive="false" timeDependent="false"> + <responseDeclaration identifier="RESPONSE_1" cardinality="single" baseType="identifier"> + <correctResponse> + <value> + id87d42b76-93d7-42fc-bdec-3e2419fa901d + </value> + </correctResponse> + </responseDeclaration> + <outcomeDeclaration identifier="SCORE" cardinality="single" baseType="float"> + <defaultValue> + <value> + 0 + </value> + </defaultValue> + </outcomeDeclaration> + <outcomeDeclaration identifier="MAXSCORE" cardinality="single" baseType="float"> + <defaultValue> + <value> + 1 + </value> + </defaultValue> + </outcomeDeclaration> + <outcomeDeclaration identifier="FEEDBACKBASIC" cardinality="single" baseType="identifier"> + <defaultValue> + <value> + empty + </value> + </defaultValue> + </outcomeDeclaration> + <outcomeDeclaration identifier="MINSCORE" cardinality="single" baseType="float" view="testConstructor"> + <defaultValue> + <value> + 0 + </value> + </defaultValue> + </outcomeDeclaration> + <outcomeDeclaration identifier="FEEDBACKMODAL" cardinality="multiple" baseType="identifier" view="testConstructor" /><templateDeclaration identifier="NEW_VARIABLE" cardinality="single" baseType="integer" /> + <templateProcessing> + <setTemplateValue identifier="NEW_VARIABLE"> + <randomInteger min="1" max="10" /> + </setTemplateValue> + <templateCondition> + <templateIf> + <gte> + <variable identifier="NEW_VARIABLE" /> + <baseValue baseType="integer"> + 0 + </baseValue> + </gte> + <setTemplateValue identifier="NEW_VARIABLE"> + <randomInteger min="1" max="10" /> + </setTemplateValue> + </templateIf> + </templateCondition> + </templateProcessing> + <itemBody> + <choiceInteraction responseIdentifier="RESPONSE_1" shuffle="true" maxChoices="1"> + <simpleChoice identifier="id87d42b76-93d7-42fc-bdec-3e2419fa901d"> + <p> + New answer + </p> + </simpleChoice> + </choiceInteraction> + </itemBody> + <responseProcessing> + <responseCondition> + <responseIf> + <isNull> + <variable identifier="RESPONSE_1" /> + </isNull> + <setOutcomeValue identifier="FEEDBACKBASIC"> + <baseValue baseType="identifier"> + empty + </baseValue> + </setOutcomeValue> + </responseIf> + <responseElseIf> + <match> + <variable identifier="RESPONSE_1" /><correct identifier="RESPONSE_1" /> + </match> + <setOutcomeValue identifier="SCORE"> + <sum> + <variable identifier="SCORE" /><variable identifier="MAXSCORE" /> + </sum> + </setOutcomeValue> + <setOutcomeValue identifier="FEEDBACKBASIC"> + <baseValue baseType="identifier"> + correct + </baseValue> + </setOutcomeValue> + </responseElseIf> + <responseElse> + <setOutcomeValue identifier="FEEDBACKBASIC"> + <baseValue baseType="identifier"> + incorrect + </baseValue> + </setOutcomeValue> + </responseElse> + </responseCondition> + <responseCondition> + <responseIf> + <and> + <match> + <baseValue baseType="identifier"> + correct + </baseValue> + <variable identifier="FEEDBACKBASIC" /> + </match> + </and> + <setOutcomeValue identifier="FEEDBACKMODAL"> + <multiple> + <variable identifier="FEEDBACKMODAL" /> + <baseValue baseType="identifier"> + Feedback261171147 + </baseValue> + </multiple> + </setOutcomeValue> + </responseIf> + </responseCondition> + <responseCondition> + <responseIf> + <and> + <match> + <baseValue baseType="identifier"> + incorrect + </baseValue> + <variable identifier="FEEDBACKBASIC" /> + </match> + </and> + <setOutcomeValue identifier="FEEDBACKMODAL"> + <multiple> + <variable identifier="FEEDBACKMODAL" /> + <baseValue baseType="identifier"> + Feedback730653886 + </baseValue> + </multiple> + </setOutcomeValue> + </responseIf> + </responseCondition> + <responseCondition> + <responseIf> + <and> + <equal toleranceMode="exact"> + <variable identifier="SCORE" /> + <baseValue baseType="float"> + 0 + </baseValue> + </equal> + </and> + <setOutcomeValue identifier="FEEDBACKMODAL"> + <multiple> + <variable identifier="FEEDBACKMODAL" /> + <baseValue baseType="identifier"> + Feedback560425559 + </baseValue> + </multiple> + </setOutcomeValue> + </responseIf> + </responseCondition> + <responseCondition> + <responseIf> + <and> + <equal toleranceMode="exact"> + <variable identifier="SCORE" /> + <baseValue baseType="float"> + 0 + </baseValue> + </equal> + <match> + <baseValue baseType="identifier"> + id87d42b76-93d7-42fc-bdec-3e2419fa901d + </baseValue> + <variable identifier="RESPONSE_1" /> + </match> + </and> + <setOutcomeValue identifier="FEEDBACKMODAL"> + <multiple> + <variable identifier="FEEDBACKMODAL" /> + <baseValue baseType="identifier"> + Feedback2007127083 + </baseValue> + </multiple> + </setOutcomeValue> + </responseIf> + </responseCondition> + <responseCondition> + <responseIf> + <gt> + <variable identifier="SCORE" /><variable identifier="MAXSCORE" /> + </gt> + <setOutcomeValue identifier="SCORE"> + <variable identifier="MAXSCORE" /> + </setOutcomeValue> + </responseIf> + </responseCondition> + <responseCondition> + <responseIf> + <lt> + <variable identifier="SCORE" /><variable identifier="MINSCORE" /> + </lt> + <setOutcomeValue identifier="SCORE"> + <variable identifier="MINSCORE" /> + </setOutcomeValue> + </responseIf> + </responseCondition> + </responseProcessing> + <modalFeedback identifier="Feedback261171147" outcomeIdentifier="FEEDBACKMODAL" showHide="show" title="Correct answer"> + <p> + This is the correct answer + </p> + </modalFeedback> + <modalFeedback identifier="Feedback730653886" outcomeIdentifier="FEEDBACKMODAL" showHide="show" title="Wong"> + <p> + This is the wrong answer + </p> + </modalFeedback> + <modalFeedback identifier="Feedback560425559" outcomeIdentifier="FEEDBACKMODAL" showHide="show" title="Answer specific"> + <p> + Answer specific feedback + </p> + </modalFeedback> + <modalFeedback identifier="Feedback2007127083" outcomeIdentifier="FEEDBACKMODAL" showHide="show" title="More specific feedback"> + <p> + Very specific feedback + </p> + </modalFeedback> +</assessmentItem>