From 453e6b590795531bae54bf4f275802dba0d99106 Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Tue, 16 Feb 2016 20:07:54 +0100 Subject: [PATCH] OO-1593: can import a question with the CSV import --- .../model/xml/AssessmentItemBuilder.java | 7 + .../model/xml/AssessmentItemFactory.java | 21 +- .../ChoiceAssessmentItemBuilder.java | 7 + .../EssayAssessmentItemBuilder.java | 12 +- .../FIBAssessmentItemBuilder.java | 101 ++++ .../KPrimAssessmentItemBuilder.java | 8 +- .../MultipleChoiceAssessmentItemBuilder.java | 14 +- .../SingleChoiceAssessmentItemBuilder.java | 10 +- .../AssessmentItemAndMetadata.java | 185 +++++++ .../AssessmentItemsPackage.java | 41 ++ .../CSVToQuestionConverter.java | 450 ++++++++++++++++++ .../qti21/questionimport/ImportOptions.java | 43 ++ .../OverviewQuestionController.java | 150 ++++++ .../questionimport/QImport_1_InputStep.java | 59 +++ .../QImport_2_OverviewStep.java | 64 +++ .../questionimport/TextInputController.java | 123 +++++ .../questionimport/_content/example.html | 4 + .../_i18n/LocalStrings_de.properties | 1 + .../_i18n/LocalStrings_en.properties | 14 + .../AssessmentTestComposerController.java | 114 ++++- .../SingleChoiceEditorController.java | 2 +- .../model/xml/AssessmentItemPackageTest.java | 2 +- 22 files changed, 1395 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemAndMetadata.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemsPackage.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/CSVToQuestionConverter.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/ImportOptions.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/OverviewQuestionController.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/QImport_1_InputStep.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/QImport_2_OverviewStep.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/TextInputController.java create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/_content/example.html create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_de.properties create mode 100644 src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_en.properties 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 index 55fe5c6eeec..04c928dd20c 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilder.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import org.olat.ims.qti21.QTI21Constants; +import org.olat.ims.qti21.model.QTI21QuestionType; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.ModalFeedback; @@ -73,6 +74,8 @@ public abstract class AssessmentItemBuilder { return assessmentItem; } + public abstract QTI21QuestionType getQuestionType(); + protected void extract() { extractMinScore(); extractMaxScore(); @@ -131,6 +134,10 @@ public abstract class AssessmentItemBuilder { assessmentItem.setTitle(title); } + public abstract String getQuestion(); + + public abstract void setQuestion(String question); + public ScoreBuilder getMinScoreBuilder() { return minScoreBuilder; } 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 c0f62ab97b4..528eddb68c6 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 @@ -31,6 +31,7 @@ import java.util.Map; import org.olat.core.helpers.Settings; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.model.IdentifierGenerator; +import org.olat.ims.qti21.model.QTI21QuestionType; import uk.ac.ed.ph.jqtiplus.group.NodeGroupList; import uk.ac.ed.ph.jqtiplus.group.item.ItemBodyGroup; @@ -92,7 +93,7 @@ import uk.ac.ed.ph.jqtiplus.value.IdentifierValue; public class AssessmentItemFactory { public static AssessmentItem createSingleChoice() { - AssessmentItem assessmentItem = createAssessmentItem("Single choice"); + AssessmentItem assessmentItem = createAssessmentItem(QTI21QuestionType.sc, "Single choice"); //define correct answer Identifier responseDeclarationId = Identifier.assumedLegal("RESPONSE_1"); @@ -106,7 +107,7 @@ public class AssessmentItemFactory { //the single choice interaction ItemBody itemBody = appendDefaultItemBody(assessmentItem); ChoiceInteraction choiceInteraction = appendChoiceInteraction(itemBody, responseDeclarationId, 1, true); - appendSimpleChoice(choiceInteraction, "sc"); + appendSimpleChoice(choiceInteraction, "New answer", "sc"); //response processing ResponseProcessing responseProcessing = createResponseProcessing(assessmentItem, responseDeclarationId); @@ -114,9 +115,13 @@ public class AssessmentItemFactory { return assessmentItem; } - public static AssessmentItem createAssessmentItem(String defaultTitle) { + public static AssessmentItem createAssessmentItem(QTI21QuestionType type, String defaultTitle) { AssessmentItem assessmentItem = new AssessmentItem(); - assessmentItem.setIdentifier(IdentifierGenerator.newAsString("item")); + if(type != null) { + assessmentItem.setIdentifier(IdentifierGenerator.newAsString(type.getPrefix())); + } else { + assessmentItem.setIdentifier(IdentifierGenerator.newAsString("item")); + } assessmentItem.setTitle(defaultTitle); assessmentItem.setToolName(QTI21Constants.TOOLNAME); assessmentItem.setToolVersion(Settings.getVersion()); @@ -309,16 +314,16 @@ public class AssessmentItemFactory { return responseDeclaration; } - public static SimpleChoice createSimpleChoice(ChoiceInteraction choiceInteraction, String prefix) { + public static SimpleChoice createSimpleChoice(ChoiceInteraction choiceInteraction, String text, String prefix) { SimpleChoice newChoice = new SimpleChoice(choiceInteraction); newChoice.setIdentifier(IdentifierGenerator.newAsIdentifier(prefix)); - P firstChoiceText = AssessmentItemFactory.getParagraph(newChoice, "New answer"); + P firstChoiceText = AssessmentItemFactory.getParagraph(newChoice, text); newChoice.getFlowStatics().add(firstChoiceText); return newChoice; } - public static SimpleChoice appendSimpleChoice(ChoiceInteraction choiceInteraction, String prefix) { - SimpleChoice newChoice = createSimpleChoice(choiceInteraction, prefix); + public static SimpleChoice appendSimpleChoice(ChoiceInteraction choiceInteraction, String text, String prefix) { + SimpleChoice newChoice = createSimpleChoice(choiceInteraction, text, prefix); choiceInteraction.getNodeGroups().getSimpleChoiceGroup().getSimpleChoices().add(newChoice); return newChoice; } diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/ChoiceAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/ChoiceAssessmentItemBuilder.java index 0623f2d9fe1..7f8ed39286d 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/ChoiceAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/ChoiceAssessmentItemBuilder.java @@ -187,6 +187,13 @@ public abstract class ChoiceAssessmentItemBuilder extends AssessmentItemBuilder return choices; } + public void addSimpleChoice(SimpleChoice choice) { + if(choices == null) { + choices = new ArrayList<>(); + } + choices.add(choice); + } + public void setSimpleChoices(List<SimpleChoice> choices) { this.choices = new ArrayList<>(choices); } diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/EssayAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/EssayAssessmentItemBuilder.java index c39d6b159e6..f6038794969 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/EssayAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/EssayAssessmentItemBuilder.java @@ -31,6 +31,7 @@ import javax.xml.transform.stream.StreamResult; import org.olat.core.gui.render.StringOutput; import org.olat.ims.qti21.QTI21Constants; +import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; import org.olat.ims.qti21.model.xml.AssessmentItemFactory; @@ -73,7 +74,7 @@ public class EssayAssessmentItemBuilder extends AssessmentItemBuilder { } private static AssessmentItem createAssessmentItem() { - AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem("Essay"); + AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem(QTI21QuestionType.essay, "Essay"); //define the response Identifier responseDeclarationId = Identifier.assumedLegal("RESPONSE_1"); @@ -92,6 +93,11 @@ public class EssayAssessmentItemBuilder extends AssessmentItemBuilder { return assessmentItem; } + @Override + public QTI21QuestionType getQuestionType() { + return QTI21QuestionType.essay; + } + @Override public void extract() { super.extract(); @@ -112,11 +118,13 @@ public class EssayAssessmentItemBuilder extends AssessmentItemBuilder { } question = sb.toString(); } - + + @Override public String getQuestion() { return question; } + @Override public void setQuestion(String html) { this.question = html; } diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java new file mode 100644 index 00000000000..9b7be7afd3c --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java @@ -0,0 +1,101 @@ +/** + * <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.interactions; + +import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendDefaultItemBody; +import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendDefaultOutcomeDeclarations; +import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendExtendedTextInteraction; +import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createExtendedTextResponseDeclaration; +import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createResponseProcessing; + +import java.util.List; + +import org.olat.ims.qti21.model.QTI21QuestionType; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.AssessmentItemFactory; + +import uk.ac.ed.ph.jqtiplus.node.content.ItemBody; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; +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.serialization.QtiSerializer; +import uk.ac.ed.ph.jqtiplus.types.Identifier; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { + + private String question; + + public FIBAssessmentItemBuilder(QtiSerializer qtiSerializer) { + super(createAssessmentItem(), qtiSerializer); + } + + public FIBAssessmentItemBuilder(AssessmentItem assessmentItem, QtiSerializer qtiSerializer) { + super(assessmentItem, qtiSerializer); + } + + private static AssessmentItem createAssessmentItem() { + AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem(QTI21QuestionType.fib, "FIB"); + + //define the response + Identifier responseDeclarationId = Identifier.assumedLegal("RESPONSE_1"); + ResponseDeclaration responseDeclaration = createExtendedTextResponseDeclaration(assessmentItem, responseDeclarationId); + assessmentItem.getNodeGroups().getResponseDeclarationGroup().getResponseDeclarations().add(responseDeclaration); + + //outcomes + appendDefaultOutcomeDeclarations(assessmentItem, 1.0d); + + ItemBody itemBody = appendDefaultItemBody(assessmentItem); + appendExtendedTextInteraction(itemBody, responseDeclarationId); + + //response processing + ResponseProcessing responseProcessing = createResponseProcessing(assessmentItem, responseDeclarationId); + assessmentItem.getNodeGroups().getResponseProcessingGroup().setResponseProcessing(responseProcessing); + return assessmentItem; + } + + @Override + public QTI21QuestionType getQuestionType() { + return QTI21QuestionType.fib; + } + + @Override + public String getQuestion() { + return question; + } + + @Override + public void setQuestion(String html) { + this.question = html; + } + + @Override + protected void buildMainScoreRule(List<ResponseRule> responseRules) { + // + } + + +} diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/KPrimAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/KPrimAssessmentItemBuilder.java index 735f5b4bc98..078e8a6a5ed 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/KPrimAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/KPrimAssessmentItemBuilder.java @@ -35,6 +35,7 @@ import javax.xml.transform.stream.StreamResult; import org.olat.core.gui.render.StringOutput; import org.olat.ims.qti21.QTI21Constants; +import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; import org.olat.ims.qti21.model.xml.AssessmentItemFactory; @@ -91,7 +92,7 @@ public class KPrimAssessmentItemBuilder extends AssessmentItemBuilder { } private static AssessmentItem createAssessmentItem() { - AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem("KPrim"); + AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem(QTI21QuestionType.kprim, "KPrim"); NodeGroupList nodeGroups = assessmentItem.getNodeGroups(); @@ -165,6 +166,11 @@ public class KPrimAssessmentItemBuilder extends AssessmentItemBuilder { question = sb.toString(); } + @Override + public QTI21QuestionType getQuestionType() { + return QTI21QuestionType.kprim; + } + public boolean isShuffle() { return shuffle; } diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/MultipleChoiceAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/MultipleChoiceAssessmentItemBuilder.java index 17bc09ecf23..0b55b9d192d 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/MultipleChoiceAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/MultipleChoiceAssessmentItemBuilder.java @@ -28,6 +28,7 @@ import java.util.List; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.model.IdentifierGenerator; +import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.xml.AssessmentItemFactory; import uk.ac.ed.ph.jqtiplus.group.NodeGroupList; @@ -83,7 +84,7 @@ public class MultipleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBui } private static AssessmentItem createAssessmentItem() { - AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem("Multiple choice"); + AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem(QTI21QuestionType.mc, "Multiple choice"); NodeGroupList nodeGroups = assessmentItem.getNodeGroups(); @@ -101,7 +102,7 @@ public class MultipleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBui ItemBody itemBody = appendDefaultItemBody(assessmentItem); ChoiceInteraction choiceInteraction = appendChoiceInteraction(itemBody, responseDeclarationId, 1, true); - appendSimpleChoice(choiceInteraction, "mc"); + appendSimpleChoice(choiceInteraction, "New answer", "mc"); //response processing ResponseProcessing responseProcessing = createResponseProcessing(assessmentItem, responseDeclarationId); @@ -134,6 +135,11 @@ public class MultipleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBui } } } + + @Override + public QTI21QuestionType getQuestionType() { + return QTI21QuestionType.mc; + } @Override public boolean isCorrect(SimpleChoice choice) { @@ -144,6 +150,10 @@ public class MultipleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBui correctAnswers.clear(); correctAnswers.addAll(identifiers); } + + public void addCorrectAnswer(Identifier identifier) { + correctAnswers.add(identifier); + } @Override protected void buildResponseDeclaration() { diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/SingleChoiceAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/SingleChoiceAssessmentItemBuilder.java index de87d1322a0..fd5cd549957 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/SingleChoiceAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/SingleChoiceAssessmentItemBuilder.java @@ -30,6 +30,7 @@ import java.util.List; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.model.IdentifierGenerator; +import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.model.xml.AssessmentItemFactory; import uk.ac.ed.ph.jqtiplus.node.content.ItemBody; @@ -82,7 +83,7 @@ public class SingleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBuild } private static AssessmentItem createAssessmentItem() { - AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem("Single choice"); + AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem(QTI21QuestionType.sc, "Single choice"); //define correct answer Identifier responseDeclarationId = Identifier.assumedLegal("RESPONSE_1"); @@ -97,7 +98,7 @@ public class SingleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBuild ItemBody itemBody = appendDefaultItemBody(assessmentItem); ChoiceInteraction choiceInteraction = appendChoiceInteraction(itemBody, responseDeclarationId, 1, true); - appendSimpleChoice(choiceInteraction, "sc"); + appendSimpleChoice(choiceInteraction, "New answer", "sc"); //response processing ResponseProcessing responseProcessing = createResponseProcessing(assessmentItem, responseDeclarationId); @@ -122,6 +123,11 @@ public class SingleChoiceAssessmentItemBuilder extends ChoiceAssessmentItemBuild } } } + + @Override + public QTI21QuestionType getQuestionType() { + return QTI21QuestionType.sc; + } @Override public boolean isCorrect(SimpleChoice choice) { diff --git a/src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemAndMetadata.java b/src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemAndMetadata.java new file mode 100644 index 00000000000..98b53ddf53d --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemAndMetadata.java @@ -0,0 +1,185 @@ +/** + * <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.questionimport; + +import java.math.BigDecimal; + +import org.olat.ims.qti21.model.QTI21QuestionType; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class AssessmentItemAndMetadata { + + private final AssessmentItemBuilder item; + + private QTI21QuestionType questionType; + private String language; + private String taxonomyPath; + private String keywords; + private String coverage; + private String level; + private String typicalLearningTime; + private String license; + private String editor; + private String editorVersion; + private int numOfAnswerAlternatives; + private BigDecimal difficulty; + private BigDecimal differentiation; + private BigDecimal stdevDifficulty; + + private boolean hasError; + + public AssessmentItemAndMetadata(AssessmentItemBuilder item) { + this.item = item; + } + + public AssessmentItemBuilder getItemBuilder() { + return item; + } + + public BigDecimal getDifficulty() { + return difficulty; + } + + public void setDifficulty(BigDecimal difficulty) { + this.difficulty = difficulty; + } + + public BigDecimal getStdevDifficulty() { + return stdevDifficulty; + } + + public void setStdevDifficulty(BigDecimal stdevDifficulty) { + this.stdevDifficulty = stdevDifficulty; + } + + public BigDecimal getDifferentiation() { + return differentiation; + } + + public void setDifferentiation(BigDecimal differentiation) { + this.differentiation = differentiation; + } + + public int getNumOfAnswerAlternatives() { + return numOfAnswerAlternatives; + } + + public void setNumOfAnswerAlternatives(int numOfAnswerAlternatives) { + this.numOfAnswerAlternatives = numOfAnswerAlternatives; + } + + public String getEditor() { + return editor; + } + + public void setEditor(String editor) { + this.editor = editor; + } + + public String getEditorVersion() { + return editorVersion; + } + + public void setEditorVersion(String editorVersion) { + this.editorVersion = editorVersion; + } + + public String getLicense() { + return license; + } + + public void setLicense(String license) { + this.license = license; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + public String getTypicalLearningTime() { + return typicalLearningTime; + } + + public void setTypicalLearningTime(String typicalLearningTime) { + this.typicalLearningTime = typicalLearningTime; + } + + public String getCoverage() { + return coverage; + } + + public void setCoverage(String coverage) { + this.coverage = coverage; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public String getKeywords() { + return keywords; + } + + public void setKeywords(String keywords) { + this.keywords = keywords; + } + + public String getTaxonomyPath() { + return taxonomyPath; + } + + public void setTaxonomyPath(String taxonomyPath) { + this.taxonomyPath = taxonomyPath; + } + + /** + * + * @return The type defined in org.olat.ims.qti.editor.beecom.objects.Question.TYPE_* + */ + public QTI21QuestionType getQuestionType() { + return questionType; + } + + public void setTitle(String title) { + item.setTitle(title); + } + + public boolean isHasError() { + return hasError; + } + + public void setHasError(boolean hasError) { + this.hasError = hasError; + } +} diff --git a/src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemsPackage.java b/src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemsPackage.java new file mode 100644 index 00000000000..826ee41a45a --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/AssessmentItemsPackage.java @@ -0,0 +1,41 @@ +/** + * <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.questionimport; + +import java.util.List; + +/** + * + * Initial date: 24.09.2014<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class AssessmentItemsPackage { + + private List<AssessmentItemAndMetadata> items; + + public List<AssessmentItemAndMetadata> getItems() { + return items; + } + + public void setItems(List<AssessmentItemAndMetadata> items) { + this.items = items; + } +} diff --git a/src/main/java/org/olat/ims/qti21/questionimport/CSVToQuestionConverter.java b/src/main/java/org/olat/ims/qti21/questionimport/CSVToQuestionConverter.java new file mode 100644 index 00000000000..a2a2dd223cc --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/CSVToQuestionConverter.java @@ -0,0 +1,450 @@ +/** + * <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.questionimport; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.core.util.StringHelper; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.AssessmentItemFactory; +import org.olat.ims.qti21.model.xml.interactions.ChoiceAssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.interactions.MultipleChoiceAssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.interactions.SingleChoiceAssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.interactions.ChoiceAssessmentItemBuilder.ScoreEvaluation; + +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; + +/** + * + * Initial date: 24.09.2014<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class CSVToQuestionConverter { + + private static final OLog log = Tracing.createLoggerFor(CSVToQuestionConverter.class); + + private ImportOptions options; + private final QtiSerializer qtiSerializer; + private AssessmentItemAndMetadata currentItem; + private final List<AssessmentItemAndMetadata> items = new ArrayList<>(); + + public CSVToQuestionConverter(ImportOptions options, QtiSerializer qtiSerializer) { + this.options = options; + this.qtiSerializer = qtiSerializer; + } + + public List<AssessmentItemAndMetadata> getItems() { + return items; + } + + public void parse(String input) { + String[] lines = input.split("\r?\n"); + + for (int i = 0; i<lines.length; i++) { + String line = lines[i]; + if (line.equals("")) { + continue; + } + + String delimiter = "\t"; + // use comma as fallback delimiter, e.g. for better testing + if (line.indexOf(delimiter) == -1) { + delimiter = ","; + } + String[] parts = line.split(delimiter); + if(parts.length > 1) { + processLine(parts); + } + } + + if(currentItem != null) { + currentItem.getItemBuilder().build(); + items.add(currentItem); + currentItem = null; + } + } + + private void processLine(String[] parts) { + String marker = parts[0].toLowerCase(); + switch(marker) { + case "typ": + case "type": processType(parts); break; + case "titel": + case "title": processTitle(parts); break; + case "beschreibung": + case "description": break; + case "frage": + case "question": processQuestion(parts); break; + case "punkte": + case "points": processPoints(parts); break; + case "fachbereich": + case "subject": processTaxonomyPath(parts); break; + case "feedback correct answer": processFeedbackCorrectAnswer(parts); break; + case "feedback wrong answer": processFeedbackWrongAnswer(parts); break; + case "schlagworte": + case "keywords": processKeywords(parts); break; + case "abdeckung": + case "coverage": processCoverage(parts); break; + case "level": processLevel(parts); break; + case "sprache": + case "language": processLanguage(parts); break; + case "durchschnittliche bearbeitungszeit": + case "typical learning time": processTypicalLearningTime(parts); break; + case "itemschwierigkeit": + case "difficulty index": processDifficultyIndex(parts); break; + case "standardabweichung itemschwierigkeit": + case "standard deviation": processStandardDeviation(parts); break; + case "trennsch\u00E4rfe": + case "discrimination index": processDiscriminationIndex(parts); break; + case "anzahl distraktoren": + case "distractors": processDistractors(parts); break; + case "editor": processEditor(parts); break; + case "editor version": processEditorVersion(parts); break; + case "lizenz": + case "license": processLicense(parts); break; + default: processChoice(parts); + } + } + + private void processLevel(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String level = parts[1]; + if(StringHelper.containsNonWhitespace(level)) { + currentItem.setLevel(level.trim()); + } + } + + private void processTypicalLearningTime(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String time = parts[1]; + if(StringHelper.containsNonWhitespace(time)) { + currentItem.setTypicalLearningTime(time.trim()); + } + } + + private void processLicense(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String license = parts[1]; + if(StringHelper.containsNonWhitespace(license)) { + currentItem.setLicense(license.trim()); + } + } + + private void processEditor(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String editor = parts[1]; + if(StringHelper.containsNonWhitespace(editor)) { + currentItem.setEditor(editor.trim()); + } + } + + private void processEditorVersion(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String editorVersion = parts[1]; + if(StringHelper.containsNonWhitespace(editorVersion)) { + currentItem.setEditorVersion(editorVersion.trim()); + } + } + + private void processFeedbackCorrectAnswer(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String feedback = parts[1]; + if(StringHelper.containsNonWhitespace(feedback)) { + AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); + itemBuilder.createCorrectFeedback().setText(feedback); + } + } + + private void processFeedbackWrongAnswer(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String feedback = parts[1]; + if(StringHelper.containsNonWhitespace(feedback)) { + AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); + itemBuilder.createIncorrectFeedback().setText(feedback); + } + } + + private void processDistractors(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String distractors = parts[1]; + if(StringHelper.containsNonWhitespace(distractors)) { + try { + currentItem.setNumOfAnswerAlternatives(Integer.parseInt(distractors.trim())); + } catch (NumberFormatException e) { + log.warn("", e); + } + } + } + + private void processDiscriminationIndex(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String discriminationIndex = parts[1]; + if(StringHelper.containsNonWhitespace(discriminationIndex)) { + try { + currentItem.setDifferentiation(new BigDecimal(discriminationIndex.trim())); + } catch (Exception e) { + log.warn("", e); + } + } + } + + private void processDifficultyIndex(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String difficulty = parts[1]; + if(StringHelper.containsNonWhitespace(difficulty)) { + try { + BigDecimal dif = new BigDecimal(difficulty.trim()); + if(dif.doubleValue() >= 0.0d && dif.doubleValue() <= 1.0d) { + currentItem.setDifficulty(dif); + } else { + currentItem.setHasError(true); + } + } catch (Exception e) { + log.warn("", e); + } + } + } + + private void processStandardDeviation(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String stddev = parts[1]; + if(StringHelper.containsNonWhitespace(stddev)) { + try { + BigDecimal dev = new BigDecimal(stddev.trim()); + if(dev.doubleValue() >= 0.0d && dev.doubleValue() <= 1.0d) { + currentItem.setStdevDifficulty(dev); + } else { + currentItem.setHasError(true); + } + } catch (Exception e) { + log.warn("", e); + } + } + } + + private void processType(String[] parts) { + if(currentItem != null) { + items.add(currentItem); + } + + if(parts.length > 1) { + String type = parts[1].toLowerCase(); + AssessmentItemBuilder itemBuilder; + switch(type) { + case "fib": { + FIBAssessmentItemBuilder fibItemBuilder = new FIBAssessmentItemBuilder(qtiSerializer); + itemBuilder = fibItemBuilder; + break; + } + case "mc": { + MultipleChoiceAssessmentItemBuilder mcItemBuilder = new MultipleChoiceAssessmentItemBuilder(qtiSerializer); + mcItemBuilder.setSimpleChoices(Collections.emptyList()); + mcItemBuilder.clearMapping(); + mcItemBuilder.setShuffle(options.isShuffle()); + mcItemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); + itemBuilder = mcItemBuilder; + break; + } + case "sc": { + SingleChoiceAssessmentItemBuilder scItemBuilder = new SingleChoiceAssessmentItemBuilder(qtiSerializer); + scItemBuilder.setSimpleChoices(Collections.emptyList()); + scItemBuilder.clearMapping(); + scItemBuilder.setShuffle(options.isShuffle()); + scItemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); + itemBuilder = scItemBuilder; + break; + } + default: { + itemBuilder = null; + } + } + + if(itemBuilder != null) { + currentItem = new AssessmentItemAndMetadata(itemBuilder); + } else { + log.warn("Question type not supported: " + type); + currentItem = null; + } + } + } + + private void processCoverage(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String coverage = parts[1]; + if(StringHelper.containsNonWhitespace(coverage)) { + currentItem.setCoverage(coverage); + } + } + + private void processKeywords(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String keywords = parts[1]; + if(StringHelper.containsNonWhitespace(keywords)) { + currentItem.setKeywords(keywords); + } + } + + private void processTaxonomyPath(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String taxonomyPath = parts[1]; + if(StringHelper.containsNonWhitespace(taxonomyPath)) { + currentItem.setTaxonomyPath(taxonomyPath); + } + } + + private void processLanguage(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String language = parts[1]; + if(StringHelper.containsNonWhitespace(language)) { + currentItem.setLanguage(language); + } + } + + private void processTitle(String[] parts) { + if(currentItem == null || parts.length < 2) return; + + String title = parts[1]; + if(StringHelper.containsNonWhitespace(title)) { + currentItem.setTitle(title); + } + } + + private void processQuestion(String[] parts) { + if(currentItem == null) return; + + String content = parts[1]; + if(StringHelper.containsNonWhitespace(content)) { + AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); + itemBuilder.setQuestion("<p>" + content + "</p>"); + } + } + + private void processPoints(String[] parts) { + if(currentItem == null) return; + + double points = parseFloat(parts[1], 1.0f); + AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); + if (itemBuilder instanceof ChoiceAssessmentItemBuilder) { + itemBuilder.setMinScore(0.0d); + itemBuilder.setMaxScore(points); + } else if(itemBuilder instanceof FIBAssessmentItemBuilder) { + itemBuilder.setMinScore(0.0d); + itemBuilder.setMaxScore(points); + } + } + + private void processChoice(String[] parts) { + if(currentItem == null || parts.length < 2) { + return; + } + + try { + AssessmentItemBuilder itemBuilder = currentItem.getItemBuilder(); + if (itemBuilder instanceof ChoiceAssessmentItemBuilder) { + double point = parseFloat(parts[0], 1.0f); + String content = parts[1]; + + ChoiceAssessmentItemBuilder choiceBuilder = (ChoiceAssessmentItemBuilder)itemBuilder; + ChoiceInteraction interaction = choiceBuilder.getChoiceInteraction(); + SimpleChoice newChoice = AssessmentItemFactory + .createSimpleChoice(interaction, content, choiceBuilder.getQuestionType().getPrefix()); + choiceBuilder.addSimpleChoice(newChoice); + choiceBuilder.setMapping(newChoice.getIdentifier(), point); + + if(point > 0.0) { + if (itemBuilder instanceof MultipleChoiceAssessmentItemBuilder) { + ((MultipleChoiceAssessmentItemBuilder)itemBuilder).addCorrectAnswer(newChoice.getIdentifier()); + } else if (itemBuilder instanceof SingleChoiceAssessmentItemBuilder) { + ((SingleChoiceAssessmentItemBuilder)itemBuilder).setCorrectAnswer(newChoice.getIdentifier()); + } + } + } else if(itemBuilder instanceof FIBAssessmentItemBuilder) { + String firstPart = parts[0].toLowerCase(); + FIBAssessmentItemBuilder fibBuilder = (FIBAssessmentItemBuilder)itemBuilder; + if("text".equals(firstPart) || "texte".equals(firstPart)) { + String text = parts[1]; + + //fibBuilder.getResponses().add(response); + } else { + float point = parseFloat(parts[0], 1.0f); + String correctBlank = parts[1]; + /* + FIBResponse response = new FIBResponse(); + response.setType(FIBResponse.TYPE_BLANK); + response.setCorrectBlank(correctBlank); + response.setPoints(point); + + if(parts.length > 2) { + String sizes = parts[2]; + String[] sizeArr = sizes.split(","); + if(sizeArr.length >= 2) { + int size = Integer.parseInt(sizeArr[0]); + int maxLength = Integer.parseInt(sizeArr[1]); + response.setSize(size); + response.setMaxLength(maxLength); + } + } + + //fibBuilder.getResponses().add(response); + + */ + } + } + } catch (NumberFormatException e) { + log.warn("Cannot parse point for: " + parts[0] + " / " + parts[1], e); + } + } + + private float parseFloat(String value, float defaultValue) { + float floatValue = defaultValue; + + if(value != null) { + if(value.indexOf(",") >= 0) { + value = value.replace(",", "."); + } + floatValue = Float.parseFloat(value); + } + return floatValue; + } +} diff --git a/src/main/java/org/olat/ims/qti21/questionimport/ImportOptions.java b/src/main/java/org/olat/ims/qti21/questionimport/ImportOptions.java new file mode 100644 index 00000000000..2f13ee140d8 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/ImportOptions.java @@ -0,0 +1,43 @@ +/** + * <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.questionimport; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ImportOptions { + + private boolean shuffle; + + public ImportOptions() { + // + } + + public boolean isShuffle() { + return shuffle; + } + + public void setShuffle(boolean shuffle) { + this.shuffle = shuffle; + } +} diff --git a/src/main/java/org/olat/ims/qti21/questionimport/OverviewQuestionController.java b/src/main/java/org/olat/ims/qti21/questionimport/OverviewQuestionController.java new file mode 100644 index 00000000000..6307000793b --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/OverviewQuestionController.java @@ -0,0 +1,150 @@ +/** + * <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.questionimport; + +import java.util.ArrayList; +import java.util.List; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.impl.Form; +import org.olat.core.gui.components.form.flexible.impl.elements.table.BooleanCellRenderer; +import org.olat.core.gui.components.form.flexible.impl.elements.table.CSSIconFlexiCellRenderer; +import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiColumnModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel; +import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.control.generic.wizard.StepFormBasicController; +import org.olat.core.gui.control.generic.wizard.StepsEvent; +import org.olat.core.gui.control.generic.wizard.StepsRunContext; +import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.ScoreBuilder; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class OverviewQuestionController extends StepFormBasicController { + + private final boolean lastStep; + private final AssessmentItemsPackage importedItems; + + public OverviewQuestionController(UserRequest ureq, WindowControl wControl, StepsRunContext runContext, + Form rootForm, AssessmentItemsPackage importedItems, boolean lastStep) { + super(ureq, wControl, rootForm, runContext, LAYOUT_VERTICAL, null); + this.lastStep = lastStep; + this.importedItems = importedItems; + initForm(ureq); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel(); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(true, Cols.hasError.i18n(), Cols.hasError.ordinal(), + false, null, FlexiColumnModel.ALIGNMENT_LEFT, + new BooleanCellRenderer( + new CSSIconFlexiCellRenderer("o_icon_failed"), + new CSSIconFlexiCellRenderer("o_icon_accept")) + )); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.type.i18n(), Cols.type.ordinal())); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.title.i18n(), Cols.title.ordinal())); + columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(Cols.points.i18n(), Cols.points.ordinal())); + + ItemsTableDataModel model = new ItemsTableDataModel(importedItems.getItems(), columnsModel); + uifactory.addTableElement(getWindowControl(), "overviewTable", model, getTranslator(), formLayout); + } + + @Override + protected void doDispose() { + // + } + + @Override + protected void formOK(UserRequest ureq) { + if(lastStep) { + fireEvent(ureq, StepsEvent.INFORM_FINISHED); + } else { + fireEvent(ureq, StepsEvent.ACTIVATE_NEXT); + } + } + + private class ItemsTableDataModel extends DefaultFlexiTableDataModel<AssessmentItemAndMetadata> { + private FlexiTableColumnModel columnModel; + + public ItemsTableDataModel(List<AssessmentItemAndMetadata> options, FlexiTableColumnModel columnModel) { + super(options, columnModel); + } + + @Override + public Object getValueAt(int row, int col) { + AssessmentItemAndMetadata importedItem = getObject(row); + AssessmentItemBuilder itemBuilder = importedItem.getItemBuilder(); + switch(Cols.values()[col]) { + case hasError: return importedItem.isHasError(); + case type: { + String typeLabel; + switch(itemBuilder.getQuestionType()) { + case sc: typeLabel = translate("item.type.sc"); break; + case mc: typeLabel = translate("item.type.mc"); break; + case fib: typeLabel = translate("item.type.fib"); break; + default: { typeLabel = "??"; } + } + return typeLabel; + } + case title: return itemBuilder.getTitle(); + case points: { + ScoreBuilder score = itemBuilder.getMaxScoreBuilder(); + if(score == null) { + return null; + } + return score.getScore(); + } + default: return itemBuilder.getAssessmentItem(); + } + } + + @Override + public ItemsTableDataModel createCopyWithEmptyList() { + return new ItemsTableDataModel(new ArrayList<AssessmentItemAndMetadata>(), columnModel); + } + } + + public static enum Cols { + hasError("table.header.status"), + type("table.header.type"), + title("table.header.title"), + points("table.header.points"); + + private final String i18n; + + private Cols(String i18n) { + this.i18n = i18n; + } + + public String i18n() { + return i18n; + } + } +} diff --git a/src/main/java/org/olat/ims/qti21/questionimport/QImport_1_InputStep.java b/src/main/java/org/olat/ims/qti21/questionimport/QImport_1_InputStep.java new file mode 100644 index 00000000000..43ee405830e --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/QImport_1_InputStep.java @@ -0,0 +1,59 @@ +/** + * <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.questionimport; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.impl.Form; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.control.generic.wizard.BasicStep; +import org.olat.core.gui.control.generic.wizard.PrevNextFinishConfig; +import org.olat.core.gui.control.generic.wizard.Step; +import org.olat.core.gui.control.generic.wizard.StepFormController; +import org.olat.core.gui.control.generic.wizard.StepsRunContext; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QImport_1_InputStep extends BasicStep { + + private final ImportOptions options; + private final AssessmentItemsPackage importedItems; + + public QImport_1_InputStep(UserRequest ureq, AssessmentItemsPackage importedItems, ImportOptions options, Step additionalStep) { + super(ureq); + this.options = options; + this.importedItems = importedItems; + setNextStep(new QImport_2_OverviewStep(ureq, importedItems, additionalStep)); + setI18nTitleAndDescr("wizard.import.input.title", "wizard.import.input.title"); + } + + @Override + public PrevNextFinishConfig getInitialPrevNextFinishConfig() { + return new PrevNextFinishConfig(false, true, false); + } + + @Override + public StepFormController getStepController(UserRequest ureq, WindowControl wControl, StepsRunContext runContext, Form form) { + return new TextInputController(ureq, wControl, runContext, form, importedItems, options); + } +} diff --git a/src/main/java/org/olat/ims/qti21/questionimport/QImport_2_OverviewStep.java b/src/main/java/org/olat/ims/qti21/questionimport/QImport_2_OverviewStep.java new file mode 100644 index 00000000000..cdad9400e01 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/QImport_2_OverviewStep.java @@ -0,0 +1,64 @@ +/** + * <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.questionimport; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.impl.Form; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.gui.control.generic.wizard.BasicStep; +import org.olat.core.gui.control.generic.wizard.PrevNextFinishConfig; +import org.olat.core.gui.control.generic.wizard.Step; +import org.olat.core.gui.control.generic.wizard.StepFormController; +import org.olat.core.gui.control.generic.wizard.StepsRunContext; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QImport_2_OverviewStep extends BasicStep { + + private final boolean nextStep; + private final AssessmentItemsPackage importedItems; + + public QImport_2_OverviewStep(UserRequest ureq, AssessmentItemsPackage importedItems, Step additionalStep) { + super(ureq); + this.importedItems = importedItems; + if(additionalStep == null) { + setNextStep(NOSTEP); + nextStep = false; + } else { + setNextStep(additionalStep); + nextStep = true; + } + setI18nTitleAndDescr("wizard.import.overview.title", "wizard.import.overview.title"); + } + + @Override + public PrevNextFinishConfig getInitialPrevNextFinishConfig() { + return new PrevNextFinishConfig(true, nextStep, !nextStep); + } + + @Override + public StepFormController getStepController(UserRequest ureq, WindowControl wControl, StepsRunContext runContext, Form form) { + return new OverviewQuestionController(ureq, wControl, runContext, form, importedItems, !nextStep); + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/questionimport/TextInputController.java b/src/main/java/org/olat/ims/qti21/questionimport/TextInputController.java new file mode 100644 index 00000000000..684c557adad --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/TextInputController.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.questionimport; + +import java.io.InputStream; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.olat.core.dispatcher.mapper.Mapper; +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.Form; +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.core.gui.control.generic.wizard.StepFormBasicController; +import org.olat.core.gui.control.generic.wizard.StepsEvent; +import org.olat.core.gui.control.generic.wizard.StepsRunContext; +import org.olat.core.gui.media.MediaResource; +import org.olat.core.gui.media.StreamedMediaResource; +import org.olat.ims.qti21.QTI21Service; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class TextInputController extends StepFormBasicController { + + private String validatedInp; + private TextElement inputElement; + + private List<AssessmentItemAndMetadata> parsedItems; + private final AssessmentItemsPackage importedItems; + private final ImportOptions options; + + @Autowired + private QTI21Service qtiService; + + public TextInputController(UserRequest ureq, WindowControl wControl, StepsRunContext runContext, Form rootForm, + AssessmentItemsPackage importedItems, ImportOptions options) { + super(ureq, wControl, rootForm, runContext, LAYOUT_VERTICAL, null); + this.importedItems = importedItems; + this.options = options; + initForm(ureq); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + setFormDescription("wizard.import.input.description"); + setFormContextHelp("Data Management#qb_import"); + + FormLayoutContainer textContainer = FormLayoutContainer.createCustomFormLayout("index", getTranslator(), velocity_root + "/example.html"); + formLayout.add(textContainer); + String mapperURI = registerMapper(ureq, new ExampleMapper()); + textContainer.contextPut("mapperURI", mapperURI); + + inputElement = uifactory.addTextAreaElement("importform", "form.importdata", -1, 10, 100, false, "", formLayout); + inputElement.setMandatory(true); + inputElement.setNotEmptyCheck("form.legende.mandatory"); + } + + @Override + protected void doDispose() { + // + } + + @Override + protected boolean validateFormLogic(UserRequest ureq) { + boolean allOk = true; + + String inp = inputElement.getValue(); + if(validatedInp == null || !validatedInp.equals(inp)) { + boolean errors = convertInputField(); + allOk &= !errors; + } + + return allOk & super.validateFormLogic(ureq); + } + + @Override + protected void formOK(UserRequest ureq) { + importedItems.setItems(parsedItems); + fireEvent(ureq, StepsEvent.ACTIVATE_NEXT); + } + + private boolean convertInputField() { + boolean importDataError = false; + CSVToQuestionConverter converter = new CSVToQuestionConverter(options, qtiService.qtiSerializer()); + converter.parse(inputElement.getValue()); + parsedItems = converter.getItems(); + return importDataError; + } + + private static class ExampleMapper implements Mapper { + @Override + public MediaResource handle(String relPath, HttpServletRequest request) { + InputStream in = TextInputController.class.getResourceAsStream("qti-import-metadata.xlsx"); + return new StreamedMediaResource(in, "ImportExample.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/questionimport/_content/example.html b/src/main/java/org/olat/ims/qti21/questionimport/_content/example.html new file mode 100644 index 00000000000..618a6423b6f --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/_content/example.html @@ -0,0 +1,4 @@ +<a href="$mapperURI/ImportSample.xlsx" target="_blank"> + <i class="o_icon o_filetype_xls o_icon-lg"></i> + $r.translate("download.example") +</a> \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_de.properties new file mode 100644 index 00000000000..6fe1d1cf71e --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_de.properties @@ -0,0 +1 @@ +#Mon Aug 25 16:10:51 CEST 2014 \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_en.properties new file mode 100644 index 00000000000..3709260284b --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/questionimport/_i18n/LocalStrings_en.properties @@ -0,0 +1,14 @@ +#Mon Aug 25 16:10:51 CEST 2014 +download.example=Template excel import +wizard.import.input.title=Data input +wizard.import.input.description=Import the questions you created based on the Excel template below via copy&paste. Select the first three columns in the Excel file and copy them to the data input field. +wizard.import.overview.title=Overview +input.title=Data input +form.importdata=Copied columns from Excel (comma separated) +table.header.status=Status +table.header.type=Question type +table.header.title=Title +table.header.points=Points +item.type.fib=$org.olat.ims.qti.editor\:item.type.fib +item.type.mc=$org.olat.ims.qti.editor\:item.type.mc +item.type.sc=$org.olat.ims.qti.editor\:item.type.sc \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java index 42fd6416c3f..ccba8473261 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java @@ -46,6 +46,10 @@ import org.olat.core.gui.control.VetoableCloseController; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.MainLayoutBasicController; import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController; +import org.olat.core.gui.control.generic.wizard.Step; +import org.olat.core.gui.control.generic.wizard.StepRunnerCallback; +import org.olat.core.gui.control.generic.wizard.StepsMainRunController; +import org.olat.core.gui.control.generic.wizard.StepsRunContext; import org.olat.core.util.Util; import org.olat.fileresource.FileResourceManager; import org.olat.ims.qti21.QTI21Constants; @@ -58,6 +62,10 @@ import org.olat.ims.qti21.model.xml.interactions.KPrimAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.MultipleChoiceAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.SingleChoiceAssessmentItemBuilder; import org.olat.ims.qti21.pool.QTI21QPoolServiceProvider; +import org.olat.ims.qti21.questionimport.AssessmentItemAndMetadata; +import org.olat.ims.qti21.questionimport.AssessmentItemsPackage; +import org.olat.ims.qti21.questionimport.ImportOptions; +import org.olat.ims.qti21.questionimport.QImport_1_InputStep; import org.olat.ims.qti21.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent; import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent; @@ -103,6 +111,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController private Controller currentEditorCtrl; private CloseableModalController cmc; private SelectItemController selectQItemCtrl; + private StepsMainRunController importTableWizard; private final LayoutMain3ColsController columnLayoutCtr; private final File unzippedDirRoot; @@ -110,6 +119,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController private ManifestType manifest; private ResolvedAssessmentTest resolvedAssessmentTest; + private final boolean survey = false; private final boolean restrictedEdit; @Autowired @@ -244,7 +254,14 @@ public class AssessmentTestComposerController extends MainLayoutBasicController if(event instanceof QItemViewEvent) { QItemViewEvent e = (QItemViewEvent)event; List<QuestionItemView> items = e.getItemList(); - doInsert(items); + doInsert(ureq, items); + } + } else if(importTableWizard == source) { + AssessmentItemsPackage importPackage = (AssessmentItemsPackage)importTableWizard.getRunContext().get("importPackage"); + getWindowControl().pop(); + cleanUp(); + if(event == Event.DONE_EVENT || event == Event.CHANGED_EVENT) { + doInsert(ureq, importPackage); } } else if(cmc == source) { cleanUp(); @@ -285,6 +302,8 @@ public class AssessmentTestComposerController extends MainLayoutBasicController doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new EssayAssessmentItemBuilder(qtiService.qtiSerializer())); } else if(importFromPoolLink == source) { doSelectQItem(ureq); + } else if(importFromTableLink == source) { + doImportTable(ureq); } } @@ -300,35 +319,40 @@ public class AssessmentTestComposerController extends MainLayoutBasicController listenTo(cmc); } - private void doInsert(List<QuestionItemView> items) { + private void doImportTable(UserRequest ureq) { + removeAsListenerAndDispose(importTableWizard); + + final AssessmentItemsPackage importPackage = new AssessmentItemsPackage(); + final ImportOptions options = new ImportOptions(); + options.setShuffle(!survey); + Step start = new QImport_1_InputStep(ureq, importPackage, options, null); + StepRunnerCallback finish = new StepRunnerCallback() { + @Override + public Step execute(UserRequest uureq, WindowControl wControl, StepsRunContext runContext) { + runContext.put("importPackage", importPackage); + return StepsMainRunController.DONE_MODIFIED; + } + }; + + importTableWizard = new StepsMainRunController(ureq, getWindowControl(), start, finish, null, + translate("tools.import.table"), "o_mi_table_import_wizard"); + listenTo(importTableWizard); + getWindowControl().pushAsModalDialog(importTableWizard.getInitialComponent()); + } + + private void doInsert(UserRequest ureq, List<QuestionItemView> items) { TreeNode selectedNode = menuTree.getSelectedNode(); TreeNode sectionNode = getNearestSection(selectedNode); String firstItemId = null; - try { + AssessmentSection section = (AssessmentSection)sectionNode.getUserObject(); for(QuestionItemView item:items) { AssessmentItem assessmentItem = qti21QPoolServiceProvider.exportToQTIEditor(item, unzippedDirRoot); - AssessmentSection section = (AssessmentSection)sectionNode.getUserObject(); - - AssessmentItemRef itemRef = new AssessmentItemRef(section); - String itemId = assessmentItem.getIdentifier(); + String itemId = doInsert(section, assessmentItem); if(firstItemId == null) { firstItemId = itemId; } - itemRef.setIdentifier(Identifier.parseString(itemId)); - File itemFile = new File(unzippedDirRoot, itemId + ".xml"); - itemRef.setHref(new URI(itemFile.getName())); - section.getSectionParts().add(itemRef); - - qtiService.persistAssessmentObject(itemFile, assessmentItem); - - URI testUri = resolvedAssessmentTest.getTestLookup().getSystemId(); - File testFile = new File(testUri); - qtiService.updateAssesmentObject(testFile, resolvedAssessmentTest); - - ManifestPackage.appendAssessmentItem(itemFile.getName(), manifest); - ManifestPackage.write(manifest, new File(unzippedDirRoot, "imsmanifest.xml")); } } catch (IOException | URISyntaxException e) { showError("error.import.question"); @@ -340,6 +364,56 @@ public class AssessmentTestComposerController extends MainLayoutBasicController TreeNode newItemNode = menuTree.getTreeModel().getNodeById(firstItemId); menuTree.setSelectedNode(newItemNode); menuTree.open(newItemNode); + partEditorFactory(ureq, newItemNode); + } + + private void doInsert(UserRequest ureq, AssessmentItemsPackage importPackage) { + TreeNode selectedNode = menuTree.getSelectedNode(); + TreeNode sectionNode = getNearestSection(selectedNode); + + String firstItemId = null; + try { + AssessmentSection section = (AssessmentSection)sectionNode.getUserObject(); + + List<AssessmentItemAndMetadata> itemsAndMetadata = importPackage.getItems(); + for(AssessmentItemAndMetadata itemAndMetadata:itemsAndMetadata) { + AssessmentItem assessmentItem = itemAndMetadata.getItemBuilder().getAssessmentItem(); + String itemId = doInsert(section, assessmentItem); + if(firstItemId == null) { + firstItemId = itemId; + } + } + } catch (URISyntaxException e) { + showError("error.import.question"); + logError("", e); + } + + updateTreeModel(); + + TreeNode newItemNode = menuTree.getTreeModel().getNodeById(firstItemId); + menuTree.setSelectedNode(newItemNode); + menuTree.open(newItemNode); + partEditorFactory(ureq, newItemNode); + } + + private String doInsert(AssessmentSection section, AssessmentItem assessmentItem) + throws URISyntaxException { + AssessmentItemRef itemRef = new AssessmentItemRef(section); + String itemId = assessmentItem.getIdentifier(); + itemRef.setIdentifier(Identifier.parseString(itemId)); + File itemFile = new File(unzippedDirRoot, itemId + ".xml"); + itemRef.setHref(new URI(itemFile.getName())); + section.getSectionParts().add(itemRef); + + qtiService.persistAssessmentObject(itemFile, assessmentItem); + + URI testUri = resolvedAssessmentTest.getTestLookup().getSystemId(); + File testFile = new File(testUri); + qtiService.updateAssesmentObject(testFile, resolvedAssessmentTest); + + ManifestPackage.appendAssessmentItem(itemFile.getName(), manifest); + ManifestPackage.write(manifest, new File(unzippedDirRoot, "imsmanifest.xml")); + return itemId; } private TreeNode doOpenFirstItem() { diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java index 7925d6d10f6..c48fdf8e328 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java @@ -235,7 +235,7 @@ public class SingleChoiceEditorController extends FormBasicController { private void doAddSimpleChoice(UserRequest ureq) { ChoiceInteraction interaction = itemBuilder.getChoiceInteraction(); - SimpleChoice newChoice = AssessmentItemFactory.createSimpleChoice(interaction, "sc"); + SimpleChoice newChoice = AssessmentItemFactory.createSimpleChoice(interaction, "New answer", "sc"); wrapAnswer(ureq, newChoice); flc.setDirty(true); } diff --git a/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemPackageTest.java b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemPackageTest.java index 282619e567d..09eb1ae61b2 100644 --- a/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemPackageTest.java +++ b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemPackageTest.java @@ -111,7 +111,7 @@ public class AssessmentItemPackageTest { } - protected AssessmentItem createAssessmentItem() { + public AssessmentItem createAssessmentItem() { AssessmentItem assessmentItem = new AssessmentItem(); assessmentItem.setIdentifier("id" + UUID.randomUUID()); assessmentItem.setTitle("Physicists"); -- GitLab