diff --git a/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java b/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java index 2a3edbd1f57c694d4c8308ebf148532e215e56fe..132cfd9ff2cc0df025715ea875acb8751014ac7e 100644 --- a/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java +++ b/src/main/java/org/olat/ims/qti/qpool/QTIMetadataConverter.java @@ -47,7 +47,7 @@ import org.olat.modules.qpool.model.QuestionItemImpl; * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ -class QTIMetadataConverter { +public class QTIMetadataConverter { private Element qtimetadata; @@ -69,7 +69,7 @@ class QTIMetadataConverter { this.educationalContextDao = educationalContextDao; } - QTIMetadataConverter(QItemTypeDAO itemTypeDao, QLicenseDAO licenseDao, + public QTIMetadataConverter(QItemTypeDAO itemTypeDao, QLicenseDAO licenseDao, TaxonomyLevelDAO taxonomyLevelDao, QEducationalContextDAO educationalContextDao) { this.licenseDao = licenseDao; this.itemTypeDao = itemTypeDao; 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 bcc5fcfc9ae685820708c9a183da2ce921f016c0..0889ffbb021476a3525e3fa97f44b366cfb9147f 100644 --- a/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java +++ b/src/main/java/org/olat/ims/qti21/manager/QTI21ServiceImpl.java @@ -609,7 +609,7 @@ public class QTI21ServiceImpl implements QTI21Service, InitializingBean, Disposa if(value instanceof NumberValue) { double score = ((NumberValue)value).doubleValue(); candidateSession.setScore(new BigDecimal(Double.toString(score))); - System.out.println("Score: " + score); + //System.out.println("Score: " + score); } } } diff --git a/src/main/java/org/olat/ims/qti21/model/QTI21QuestionType.java b/src/main/java/org/olat/ims/qti21/model/QTI21QuestionType.java new file mode 100644 index 0000000000000000000000000000000000000000..2ec4cc1b60b5912e208833607c189b32d92a940c --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/QTI21QuestionType.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.model; + +import org.olat.modules.qpool.QuestionType; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public enum QTI21QuestionType { + sc(true, "sc", QuestionType.SC), + mc(true, "mc", QuestionType.MC), + kprim(true, "kprim", QuestionType.KPRIM), + fib(false, "fib", QuestionType.FIB), + essay(true, "essay", QuestionType.ESSAY), + unkown(false, null, null); + + private final String prefix; + private final boolean editor; + private final QuestionType poolQuestionType; + + private QTI21QuestionType(boolean editor, String prefix, QuestionType poolQuestionType) { + this.editor = editor; + this.prefix = prefix; + this.poolQuestionType = poolQuestionType; + } + + public boolean hasEditor() { + return editor; + } + + public String getPrefix() { + return prefix; + } + + public QuestionType getPoolQuestionType() { + return poolQuestionType; + } +} diff --git a/src/main/java/org/olat/ims/qti21/model/QTI21QuestionTypeDetector.java b/src/main/java/org/olat/ims/qti21/model/QTI21QuestionTypeDetector.java new file mode 100644 index 0000000000000000000000000000000000000000..a49c9cc9bdca332203b0550aa7c8a3418a39746b --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/QTI21QuestionTypeDetector.java @@ -0,0 +1,136 @@ +/** + * <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; + +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.interaction.ChoiceInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.ExtendedTextInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.MatchInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; +import uk.ac.ed.ph.jqtiplus.value.Cardinality; + +/** + * Retrieve the type of question (not interaction) of a recognized tool + * vendor. + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QTI21QuestionTypeDetector { + + public static String generateNewIdentifier(String identifier) { + String newIdentifier = null; + for(QTI21QuestionType type:QTI21QuestionType.values()) { + String prefix = type.getPrefix(); + if(prefix != null && identifier.startsWith(prefix)) { + newIdentifier = IdentifierGenerator.newAsString(prefix); + } + } + + if(newIdentifier == null) { + newIdentifier = IdentifierGenerator.newAsString(); + } + + return newIdentifier; + } + + public static QTI21QuestionType getType(AssessmentItem item) { + + if(QTI21Constants.TOOLNAME.equals(item.getToolName())) { + //we have create this one + List<Interaction> interactions = item.getItemBody().findInteractions(); + + boolean choice = false; + boolean match = false; + boolean essay = false; + boolean unkown = false; + + if(interactions != null && interactions.size() > 0) { + for(Interaction interaction: interactions) { + if(interaction instanceof ChoiceInteraction) { + choice = true; + } else if(interaction instanceof MatchInteraction) { + match = true; + } else if(interaction instanceof ExtendedTextInteraction) { + essay = true; + } else { + unkown = true; + } + } + } + + if(unkown) { + return QTI21QuestionType.unkown; + } else if(choice && !match && !essay && !unkown) { + return getTypeOfChoice(item); + } else if(!choice && match && !essay && !unkown) { + return getTypeOfMatch(item); + } else if(!choice && !match && essay && !unkown) { + return QTI21QuestionType.essay; + } else { + return QTI21QuestionType.unkown; + } + } else { + return QTI21QuestionType.unkown; + } + } + + private static final QTI21QuestionType getTypeOfMatch(AssessmentItem item) { + if(item.getResponseDeclarations().size() == 1) { + ResponseDeclaration responseDeclaration = item.getResponseDeclarations().get(0); + String responseIdentifier = responseDeclaration.getIdentifier().toString(); + Cardinality cardinalty = responseDeclaration.getCardinality(); + if(cardinalty.isMultiple()) { + if(responseIdentifier.startsWith("KPRIM_")) { + return QTI21QuestionType.kprim; + } else { + return QTI21QuestionType.unkown; + } + } else { + return QTI21QuestionType.unkown; + } + } else { + return QTI21QuestionType.unkown; + } + } + + private static final QTI21QuestionType getTypeOfChoice(AssessmentItem item) { + if(item.getResponseDeclarations().size() == 1) { + ResponseDeclaration responseDeclaration = item.getResponseDeclarations().get(0); + Cardinality cardinalty = responseDeclaration.getCardinality(); + if(cardinalty.isSingle()) { + return QTI21QuestionType.sc; + } else if(cardinalty.isMultiple()) { + return QTI21QuestionType.mc; + } else { + return QTI21QuestionType.unkown; + } + } else { + return QTI21QuestionType.unkown; + } + } + +} diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..c983ebe4502c0d341529741287094242191c19b8 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java @@ -0,0 +1,180 @@ +/** + * <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.math.BigDecimal; + +import org.olat.ims.qti21.model.QTI21QuestionType; + +/** + * + * Initial date: 16.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class AssessmentItemMetadata { + + 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; + + private QTI21QuestionType questionType; + private String interactionType; + + public QTI21QuestionType getQuestionType() { + return questionType; + } + + public void setQuestionType(QTI21QuestionType questionType) { + this.questionType = questionType; + } + + public String getInteractionType() { + return interactionType; + } + + public void setInteractionType(String interactionType) { + this.interactionType = interactionType; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public String getTaxonomyPath() { + return taxonomyPath; + } + + public void setTaxonomyPath(String taxonomyPath) { + this.taxonomyPath = taxonomyPath; + } + + public String getKeywords() { + return keywords; + } + + public void setKeywords(String keywords) { + this.keywords = keywords; + } + + public String getCoverage() { + return coverage; + } + + public void setCoverage(String coverage) { + this.coverage = coverage; + } + + 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 getLicense() { + return license; + } + + public void setLicense(String license) { + this.license = license; + } + + 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 int getNumOfAnswerAlternatives() { + return numOfAnswerAlternatives; + } + + public void setNumOfAnswerAlternatives(int numOfAnswerAlternatives) { + this.numOfAnswerAlternatives = numOfAnswerAlternatives; + } + + public BigDecimal getDifficulty() { + return difficulty; + } + + public void setDifficulty(BigDecimal difficulty) { + this.difficulty = difficulty; + } + + public BigDecimal getDifferentiation() { + return differentiation; + } + + public void setDifferentiation(BigDecimal differentiation) { + this.differentiation = differentiation; + } + + public BigDecimal getStdevDifficulty() { + return stdevDifficulty; + } + + public void setStdevDifficulty(BigDecimal stdevDifficulty) { + this.stdevDifficulty = stdevDifficulty; + } + + public boolean isHasError() { + return hasError; + } + + public void setHasError(boolean hasError) { + this.hasError = hasError; + } +} diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21AssessmentItemFactory.java b/src/main/java/org/olat/ims/qti21/pool/QTI21AssessmentItemFactory.java index df5e3703be4af4456c161e9b04f9d2871613c855..81c5fd64d73f3e843f51b7caf68507934bc71742 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21AssessmentItemFactory.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21AssessmentItemFactory.java @@ -26,6 +26,7 @@ import org.olat.core.gui.UserRequest; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.translator.Translator; import org.olat.core.util.Util; +import org.olat.ims.qti21.model.QTI21QuestionType; import org.olat.ims.qti21.ui.editor.AssessmentItemEditorController; import org.olat.modules.qpool.QItemFactory; import org.olat.modules.qpool.QPoolItemEditorController; @@ -39,9 +40,9 @@ import org.olat.modules.qpool.QuestionItem; */ public class QTI21AssessmentItemFactory implements QItemFactory { - private final Type type; + private final QTI21QuestionType type; - public QTI21AssessmentItemFactory(Type type) { + public QTI21AssessmentItemFactory(QTI21QuestionType type) { this.type = type; } @@ -66,11 +67,4 @@ public class QTI21AssessmentItemFactory implements QItemFactory { return ctrl; } - public enum Type { - sc, - mc, - kprim, - //fib, - essay - } } diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java b/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java index ae961b226c7b05c375c6f361fefaaf0f01b19047..19fa9036e05cdbf69717da0f33891ab08f488fa4 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java @@ -19,12 +19,22 @@ */ package org.olat.ims.qti21.pool; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.zip.ZipOutputStream; +import org.olat.core.util.FileUtils; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.ims.qti21.QTI21Service; import org.olat.modules.qpool.QuestionItemFull; import org.olat.modules.qpool.manager.QPoolFileStorage; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; + /** * * Initial date: 05.02.2016<br> @@ -32,14 +42,93 @@ import org.olat.modules.qpool.manager.QPoolFileStorage; * */ public class QTI21ExportProcessor { + + private final QTI21Service qtiService; + private final QPoolFileStorage qpoolFileStorage; - public QTI21ExportProcessor(QPoolFileStorage qpoolFileStorage) { - // TODO Auto-generated constructor stub + public QTI21ExportProcessor(QTI21Service qtiService, QPoolFileStorage qpoolFileStorage) { + this.qtiService = qtiService; + this.qpoolFileStorage = qpoolFileStorage; } public void process(QuestionItemFull item, ZipOutputStream zout, Set<String> names) { - // TODO Auto-generated method stub + // + } + + public ResolvedAssessmentItem exportToQTIEditor(QuestionItemFull fullItem, File editorContainer) + throws IOException { + AssessmentItemsAndResources itemAndMaterials = new AssessmentItemsAndResources(); + collectMaterials(fullItem, itemAndMaterials); + if(itemAndMaterials.getAssessmentItems().isEmpty()) { + return null;//nothing found + } + ResolvedAssessmentItem assessmentItem = itemAndMaterials.getAssessmentItems().get(0); + //write materials + for(ItemMaterial material:itemAndMaterials.getMaterials()) { + String exportPath = material.getExportUri(); + File leaf = new File(editorContainer, exportPath); + FileUtils.bcopy(leaf, editorContainer, "Export to QTI 2.1 editor"); + } + return assessmentItem; + } + + protected void collectMaterials(QuestionItemFull fullItem, AssessmentItemsAndResources materials) { + String dir = fullItem.getDirectory(); + String rootFilename = fullItem.getRootFilename(); + File resourceDirectory = qpoolFileStorage.getDirectory(dir); + File itemFile = new File(resourceDirectory, rootFilename); + + if(itemFile.exists()) { + ResolvedAssessmentItem assessmentItem = qtiService.loadAndResolveAssessmentItem(itemFile.toURI(), resourceDirectory); + //enrichScore(itemEl); + //enrichWithMetadata(fullItem, itemEl); + //collectResources(itemEl, container, materials); + materials.addItemEl(assessmentItem); + } } + private static final class AssessmentItemsAndResources { + private final Set<String> paths = new HashSet<String>(); + private final List<ResolvedAssessmentItem> itemEls = new ArrayList<ResolvedAssessmentItem>(); + private final List<ItemMaterial> materials = new ArrayList<ItemMaterial>(); + + public Set<String> getPaths() { + return paths; + } + + public List<ResolvedAssessmentItem> getAssessmentItems() { + return itemEls; + } + + public void addItemEl(ResolvedAssessmentItem el) { + itemEls.add(el); + } + + public List<ItemMaterial> getMaterials() { + return materials; + } + + public void addMaterial(ItemMaterial material) { + materials.add(material); + } + } + + private static final class ItemMaterial { + private final VFSLeaf leaf; + private final String exportUri; + + public ItemMaterial(VFSLeaf leaf, String exportUri) { + this.leaf = leaf; + this.exportUri = exportUri; + } + + public VFSLeaf getLeaf() { + return leaf; + } + + public String getExportUri() { + return exportUri; + } + } } diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java index 3a454dda847a0bf18f1f2224b63413562e26b167..3266b5ca2d7893a1b7a532fb49c5007d39c1cbf2 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java @@ -26,17 +26,22 @@ import java.util.Locale; import org.olat.core.commons.persistence.DB; import org.olat.core.id.Identity; import org.olat.core.util.StringHelper; -import org.olat.ims.qti.questionimport.ItemAndMetadata; +import org.olat.ims.qti.qpool.QTIMetadataConverter; import org.olat.ims.qti21.QTI21Constants; +import org.olat.ims.qti21.model.QTI21QuestionType; +import org.olat.ims.qti21.model.xml.AssessmentItemMetadata; import org.olat.modules.qpool.QuestionItem; import org.olat.modules.qpool.QuestionType; +import org.olat.modules.qpool.TaxonomyLevel; import org.olat.modules.qpool.manager.QEducationalContextDAO; import org.olat.modules.qpool.manager.QItemTypeDAO; import org.olat.modules.qpool.manager.QLicenseDAO; import org.olat.modules.qpool.manager.QPoolFileStorage; import org.olat.modules.qpool.manager.QuestionItemDAO; import org.olat.modules.qpool.manager.TaxonomyLevelDAO; +import org.olat.modules.qpool.model.QEducationalContext; import org.olat.modules.qpool.model.QItemType; +import org.olat.modules.qpool.model.QLicense; import org.olat.modules.qpool.model.QuestionItemImpl; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; @@ -53,8 +58,11 @@ public class QTI21ImportProcessor { private final Locale defaultLocale; private final QItemTypeDAO qItemTypeDao; + private final QLicenseDAO qLicenseDao; private final QuestionItemDAO questionItemDao; private final QPoolFileStorage qpoolFileStorage; + private final TaxonomyLevelDAO taxonomyLevelDao; + private final QEducationalContextDAO qEduContextDao; public QTI21ImportProcessor(Identity owner, Locale defaultLocale, String filename, File file, QuestionItemDAO questionItemDao, QItemTypeDAO qItemTypeDao, QEducationalContextDAO qEduContextDao, @@ -62,9 +70,12 @@ public class QTI21ImportProcessor { DB dbInstance) { this.owner = owner; this.defaultLocale = defaultLocale; + this.qLicenseDao = qLicenseDao; this.qItemTypeDao = qItemTypeDao; + this.qEduContextDao = qEduContextDao; this.questionItemDao = questionItemDao; this.qpoolFileStorage = qpoolFileStorage; + this.taxonomyLevelDao = taxonomyLevelDao; } public List<QuestionItem> process() { @@ -73,7 +84,7 @@ public class QTI21ImportProcessor { protected QuestionItemImpl processItem(AssessmentItem assessmentItem, String comment, String originalItemFilename, - String editor, String editorVersion, ItemAndMetadata metadata) { + String editor, String editorVersion, AssessmentItemMetadata metadata) { //filename String filename; String ident = assessmentItem.getIdentifier(); @@ -106,7 +117,7 @@ public class QTI21ImportProcessor { poolItem.setEditorVersion(editorVersion); } //if question type not found, can be overridden by the metadatas - //processItemMetadata(poolItem, itemEl); + processItemMetadata(poolItem, metadata); if(poolItem.getType() == null) { QItemType defType = qItemTypeDao.loadByType(QuestionType.UNKOWN.name()); poolItem.setType(defType); @@ -120,4 +131,80 @@ public class QTI21ImportProcessor { questionItemDao.persist(owner, poolItem); return poolItem; } + + protected void processItemMetadata(QuestionItemImpl poolItem, AssessmentItemMetadata metadata) { + //non heuristic set of question type + String typeStr = null; + QTI21QuestionType questionType = metadata.getQuestionType(); + if(questionType != null && questionType.getPoolQuestionType() != null) { + typeStr = questionType.getPoolQuestionType().name(); + } + if(typeStr != null) { + QItemType type = qItemTypeDao.loadByType(typeStr); + if(type != null) { + poolItem.setType(type); + } + } + + String coverage = metadata.getCoverage(); + if(StringHelper.containsNonWhitespace(coverage)) { + poolItem.setCoverage(coverage); + } + + String language = metadata.getLanguage(); + if(StringHelper.containsNonWhitespace(language)) { + poolItem.setLanguage(language); + } + + String keywords = metadata.getKeywords(); + if(StringHelper.containsNonWhitespace(keywords)) { + poolItem.setKeywords(keywords); + } + + String taxonomyPath = metadata.getTaxonomyPath(); + if(StringHelper.containsNonWhitespace(taxonomyPath)) { + QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao); + TaxonomyLevel taxonomyLevel = converter.toTaxonomy(taxonomyPath); + poolItem.setTaxonomyLevel(taxonomyLevel); + } + + String level = metadata.getLevel(); + if(StringHelper.containsNonWhitespace(level)) { + QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao); + QEducationalContext educationalContext = converter.toEducationalContext(level); + poolItem.setEducationalContext(educationalContext); + } + + String time = metadata.getTypicalLearningTime(); + if(StringHelper.containsNonWhitespace(time)) { + poolItem.setEducationalLearningTime(time); + } + + String editor = metadata.getEditor(); + if(StringHelper.containsNonWhitespace(editor)) { + poolItem.setEditor(editor); + } + + String editorVersion = metadata.getEditorVersion(); + if(StringHelper.containsNonWhitespace(editorVersion)) { + poolItem.setEditorVersion(editorVersion); + } + + int numOfAnswerAlternatives = metadata.getNumOfAnswerAlternatives(); + if(numOfAnswerAlternatives > 0) { + poolItem.setNumOfAnswerAlternatives(numOfAnswerAlternatives); + } + + poolItem.setDifficulty(metadata.getDifficulty()); + poolItem.setDifferentiation(metadata.getDifferentiation()); + poolItem.setStdevDifficulty(metadata.getStdevDifficulty()); + + String license = metadata.getLicense(); + if(StringHelper.containsNonWhitespace(license)) { + QTIMetadataConverter converter = new QTIMetadataConverter(qItemTypeDao, qLicenseDao, taxonomyLevelDao, qEduContextDao); + QLicense qLicense = converter.toLicense(license); + poolItem.setLicense(qLicense); + } + + } } diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java b/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java index 39197ca44abf8486d6db971f3f9f50243a4dad50..efe451626696fadcabbedf86f912442d858cc317 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java @@ -20,6 +20,7 @@ package org.olat.ims.qti21.pool; import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; @@ -45,13 +46,15 @@ import org.olat.core.util.vfs.VFSManager; import org.olat.ims.qti.fileresource.TestFileResource; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.QTI21Service; +import org.olat.ims.qti21.model.QTI21QuestionType; +import org.olat.ims.qti21.model.QTI21QuestionTypeDetector; import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.AssessmentItemMetadata; import org.olat.ims.qti21.model.xml.ManifestPackage; import org.olat.ims.qti21.model.xml.interactions.EssayAssessmentItemBuilder; 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.QTI21AssessmentItemFactory.Type; import org.olat.ims.resources.IMSEntityResolver; import org.olat.imscp.xml.manifest.ManifestType; import org.olat.modules.qpool.ExportFormatOptions; @@ -76,6 +79,7 @@ import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; /** * @@ -149,8 +153,10 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { @Override public List<QItemFactory> getItemfactories() { List<QItemFactory> factories = new ArrayList<QItemFactory>(); - for(Type type:Type.values()) { - factories.add(new QTI21AssessmentItemFactory(type)); + for(QTI21QuestionType type:QTI21QuestionType.values()) { + if(type.hasEditor()) { + factories.add(new QTI21AssessmentItemFactory(type)); + } } return factories; } @@ -200,7 +206,7 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { @Override public void exportItem(QuestionItemFull item, ZipOutputStream zout, Set<String> names) { - QTI21ExportProcessor processor = new QTI21ExportProcessor(qpoolFileStorage); + QTI21ExportProcessor processor = new QTI21ExportProcessor(qtiService, qpoolFileStorage); processor.process(item, zout, names); } @@ -227,7 +233,7 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { return editorCtrl; } - public QuestionItem createItem(Identity identity, Type type, String title, Locale locale) { + public QuestionItem createItem(Identity identity, QTI21QuestionType type, String title, Locale locale) { AssessmentItemBuilder itemBuilder = null; switch(type) { case sc: itemBuilder = new SingleChoiceAssessmentItemBuilder(qtiService.qtiSerializer()); break; @@ -242,9 +248,12 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { assessmentItem.setLabel(title); assessmentItem.setTitle(title); + AssessmentItemMetadata itemMetadata = new AssessmentItemMetadata(); + itemMetadata.setQuestionType(type); + QTI21ImportProcessor processor = new QTI21ImportProcessor(identity, locale, null, null, questionItemDao, qItemTypeDao, qEduContextDao, taxonomyLevelDao, qLicenseDao, qpoolFileStorage, dbInstance); - QuestionItemImpl qitem = processor.processItem(assessmentItem, "", null, "OpenOLAT", Settings.getVersion(), null); + QuestionItemImpl qitem = processor.processItem(assessmentItem, "", null, "OpenOLAT", Settings.getVersion(), itemMetadata); VFSContainer baseDir = qpoolFileStorage.getContainer(qitem.getDirectory()); VFSLeaf leaf = baseDir.createChildLeaf(qitem.getRootFilename()); @@ -258,4 +267,22 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { return qitem; } + + /** + * Export to QTI editor an item from the pool. The ident of the item + * is always regenerated as an UUID. + * @param qitem + * @param editorContainer + * @return + */ + public AssessmentItem exportToQTIEditor(QuestionItemShort qitem, File editorContainer) throws IOException { + QTI21ExportProcessor processor = new QTI21ExportProcessor(qtiService, qpoolFileStorage); + QuestionItemFull fullItem = questionItemDao.loadById(qitem.getKey()); + ResolvedAssessmentItem resolvedAssessmentItem = processor.exportToQTIEditor(fullItem, editorContainer); + AssessmentItem assessmentItem = resolvedAssessmentItem.getItemLookup().extractAssumingSuccessful(); + assessmentItem.setIdentifier(QTI21QuestionTypeDetector.generateNewIdentifier(assessmentItem.getIdentifier())); + return assessmentItem; + } + + } \ No newline at end of file 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 c815989b441ffbfb2a9a47a2af1f4afb2743c66b..20560c67740e3b742548362884f13dc64cab5dd0 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 @@ -21,7 +21,6 @@ package org.olat.ims.qti21.ui.editor; import java.io.File; import java.net.URI; -import java.util.List; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; @@ -31,8 +30,9 @@ import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; 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.QTI21QuestionType; +import org.olat.ims.qti21.model.QTI21QuestionTypeDetector; import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.EssayAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.KPrimAssessmentItemBuilder; @@ -50,14 +50,8 @@ import org.olat.repository.RepositoryEntry; import org.springframework.beans.factory.annotation.Autowired; 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.ExtendedTextInteraction; -import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; -import uk.ac.ed.ph.jqtiplus.node.item.interaction.MatchInteraction; -import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; -import uk.ac.ed.ph.jqtiplus.value.Cardinality; /** * @@ -136,44 +130,19 @@ public class AssessmentItemEditorController extends BasicController { } private void initItemEditor(UserRequest ureq) { + + + AssessmentItem item = resolvedAssessmentItem.getItemLookup().getRootNodeHolder().getRootNode(); - if(QTI21Constants.TOOLNAME.equals(item.getToolName())) { - //we have create this one - List<Interaction> interactions = item.getItemBody().findInteractions(); - - boolean choice = false; - boolean match = false; - boolean essay = false; - boolean unkown = false; - - if(interactions != null && interactions.size() > 0) { - for(Interaction interaction: interactions) { - if(interaction instanceof ChoiceInteraction) { - choice = true; - } else if(interaction instanceof MatchInteraction) { - match = true; - } else if(interaction instanceof ExtendedTextInteraction) { - essay = true; - } else { - unkown = true; - } - } - } - - if(unkown) { - initItemCreatedByUnkownEditor(ureq); - } else if(choice && !match && !essay && !unkown) { - itemBuilder = initChoiceEditors(ureq, item); - } else if(!choice && match && !essay && !unkown) { - itemBuilder = initMatchEditors(ureq, item); - } else if(!choice && !match && essay && !unkown) { - itemBuilder = initEssayEditors(ureq, item); - } else { - initItemCreatedByUnkownEditor(ureq); - } - } else { - initItemCreatedByUnkownEditor(ureq); + QTI21QuestionType type = QTI21QuestionTypeDetector.getType(item); + switch(type) { + case sc: itemBuilder = initSingleChoiceEditors(ureq, item); break; + case mc: itemBuilder = initMultipleChoiceEditors(ureq, item); break; + case kprim: itemBuilder = initKPrimChoiceEditors(ureq, item); break; + case essay: itemBuilder = initEssayEditors(ureq, item); break; + default: initItemCreatedByUnkownEditor(ureq); break; } + } private void initItemCreatedByUnkownEditor(UserRequest ureq) { @@ -181,44 +150,7 @@ public class AssessmentItemEditorController extends BasicController { listenTo(itemEditor); tabbedPane.addTab("Unkown", itemEditor.getInitialComponent()); } - - private AssessmentItemBuilder initMatchEditors(UserRequest ureq, AssessmentItem item) { - if(item.getResponseDeclarations().size() == 1) { - ResponseDeclaration responseDeclaration = item.getResponseDeclarations().get(0); - String responseIdentifier = responseDeclaration.getIdentifier().toString(); - Cardinality cardinalty = responseDeclaration.getCardinality(); - if(cardinalty.isMultiple()) { - if(responseIdentifier.startsWith("KPRIM_")) { - return initKPrimChoiceEditors(ureq, item); - } else { - initItemCreatedByUnkownEditor(ureq); - } - } else { - initItemCreatedByUnkownEditor(ureq); - } - } else { - initItemCreatedByUnkownEditor(ureq); - } - return null; - } - private AssessmentItemBuilder initChoiceEditors(UserRequest ureq, AssessmentItem item) { - if(item.getResponseDeclarations().size() == 1) { - ResponseDeclaration responseDeclaration = item.getResponseDeclarations().get(0); - Cardinality cardinalty = responseDeclaration.getCardinality(); - if(cardinalty.isSingle()) { - return initSingleChoiceEditors(ureq, item); - } else if(cardinalty.isMultiple()) { - return initMultipleChoiceEditors(ureq, item); - } else { - initItemCreatedByUnkownEditor(ureq); - } - } else { - initItemCreatedByUnkownEditor(ureq); - } - return null; - } - private AssessmentItemBuilder initSingleChoiceEditors(UserRequest ureq, AssessmentItem item) { SingleChoiceAssessmentItemBuilder scItemBuilder = new SingleChoiceAssessmentItemBuilder(item, qtiService.qtiSerializer()); itemEditor = new SingleChoiceEditorController(ureq, getWindowControl(), scItemBuilder); 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 f332c5c18e9cd6b618286a133c3d175c17327d91..42fd6416c3f5481a61304fd2fb17c02b81f5aa8a 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 @@ -20,8 +20,10 @@ package org.olat.ims.qti21.ui.editor; import java.io.File; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; import java.util.UUID; import org.olat.core.commons.fullWebApp.LayoutMain3ColsController; @@ -43,8 +45,10 @@ import org.olat.core.gui.control.Event; 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.util.Util; import org.olat.fileresource.FileResourceManager; +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.AssessmentTestFactory; @@ -53,12 +57,16 @@ import org.olat.ims.qti21.model.xml.interactions.EssayAssessmentItemBuilder; 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.ui.AssessmentTestDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent; import org.olat.ims.qti21.ui.editor.events.AssessmentSectionEvent; import org.olat.ims.qti21.ui.editor.events.AssessmentTestEvent; import org.olat.ims.qti21.ui.editor.events.AssessmentTestPartEvent; import org.olat.imscp.xml.manifest.ManifestType; +import org.olat.modules.qpool.QuestionItemView; +import org.olat.modules.qpool.ui.SelectItemController; +import org.olat.modules.qpool.ui.events.QItemViewEvent; import org.olat.repository.RepositoryEntry; import org.olat.repository.ui.RepositoryEntryRuntimeController.ToolbarAware; import org.springframework.beans.factory.annotation.Autowired; @@ -88,10 +96,13 @@ public class AssessmentTestComposerController extends MainLayoutBasicController private final Link saveLink; private final Dropdown addItemTools; private final Link newSectionLink, newSingleChoiceLink, newMultipleChoiceLink, newKPrimLink, newEssayLink; + private final Link importFromPoolLink, importFromTableLink; private final TooledStackedPanel toolbar; private final VelocityContainer mainVC; private Controller currentEditorCtrl; + private CloseableModalController cmc; + private SelectItemController selectQItemCtrl; private final LayoutMain3ColsController columnLayoutCtr; private final File unzippedDirRoot; @@ -103,6 +114,8 @@ public class AssessmentTestComposerController extends MainLayoutBasicController @Autowired private QTI21Service qtiService; + @Autowired + private QTI21QPoolServiceProvider qti21QPoolServiceProvider; public AssessmentTestComposerController(UserRequest ureq, WindowControl wControl, TooledStackedPanel toolbar, RepositoryEntry testEntry) { @@ -154,6 +167,16 @@ public class AssessmentTestComposerController extends MainLayoutBasicController newEssayLink = LinkFactory.createToolLink("new.essay", translate("new.essay"), this, "o_mi_qtiessay"); newEssayLink.setDomReplacementWrapperRequired(false); addItemTools.addComponent(newEssayLink); + + addItemTools.addComponent(new Dropdown.Spacer("sep-import")); + //import + importFromPoolLink = LinkFactory.createToolLink("import.pool", translate("tools.import.qpool"), this, "o_mi_qpool_import"); + importFromPoolLink.setDomReplacementWrapperRequired(false); + addItemTools.addComponent(importFromPoolLink); + importFromTableLink = LinkFactory.createToolLink("import.table", translate("tools.import.table"), this, "o_mi_table_import"); + importFromTableLink.setIconLeftCSS("o_icon o_icon_table o_icon-fw"); + importFromTableLink.setDomReplacementWrapperRequired(false); + addItemTools.addComponent(importFromTableLink); // main layout mainVC = createVelocityContainer("assessment_test_composer"); @@ -214,9 +237,27 @@ public class AssessmentTestComposerController extends MainLayoutBasicController if(AssessmentItemEvent.ASSESSMENT_ITEM_CHANGED.equals(aie.getCommand())) { doUpdate(aie.getAssessmentItemRef().getIdentifier(), aie.getAssessmentItem().getTitle()); } + } else if(selectQItemCtrl == source) { + cmc.deactivate(); + cleanUp(); + + if(event instanceof QItemViewEvent) { + QItemViewEvent e = (QItemViewEvent)event; + List<QuestionItemView> items = e.getItemList(); + doInsert(items); + } + } else if(cmc == source) { + cleanUp(); } super.event(ureq, source, event); } + + private void cleanUp() { + removeAsListenerAndDispose(selectQItemCtrl); + removeAsListenerAndDispose(cmc); + selectQItemCtrl = null; + cmc = null; + } @Override protected void event(UserRequest ureq, Component source, Event event) { @@ -242,9 +283,65 @@ public class AssessmentTestComposerController extends MainLayoutBasicController doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new KPrimAssessmentItemBuilder(qtiService.qtiSerializer())); } else if(newEssayLink == source) { doNewAssessmentItem(ureq, menuTree.getSelectedNode(), new EssayAssessmentItemBuilder(qtiService.qtiSerializer())); + } else if(importFromPoolLink == source) { + doSelectQItem(ureq); } } + private void doSelectQItem(UserRequest ureq) { + removeAsListenerAndDispose(cmc); + removeAsListenerAndDispose(selectQItemCtrl); + + selectQItemCtrl = new SelectItemController(ureq, getWindowControl(), QTI21Constants.QTI_21_FORMAT); + listenTo(selectQItemCtrl); + + cmc = new CloseableModalController(getWindowControl(), translate("close"), selectQItemCtrl.getInitialComponent(), true, translate("title.add") ); + cmc.activate(); + listenTo(cmc); + } + + private void doInsert(List<QuestionItemView> items) { + TreeNode selectedNode = menuTree.getSelectedNode(); + TreeNode sectionNode = getNearestSection(selectedNode); + + String firstItemId = null; + + try { + for(QuestionItemView item:items) { + AssessmentItem assessmentItem = qti21QPoolServiceProvider.exportToQTIEditor(item, unzippedDirRoot); + AssessmentSection section = (AssessmentSection)sectionNode.getUserObject(); + + AssessmentItemRef itemRef = new AssessmentItemRef(section); + String itemId = assessmentItem.getIdentifier(); + 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"); + logError("", e); + } + + updateTreeModel(); + + TreeNode newItemNode = menuTree.getTreeModel().getNodeById(firstItemId); + menuTree.setSelectedNode(newItemNode); + menuTree.open(newItemNode); + } + private TreeNode doOpenFirstItem() { TreeNode node = menuTree.getTreeModel().getRootNode(); if(node.getChildCount() > 0) { @@ -340,7 +437,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController partEditorFactory(ureq, newItemNode); } catch (URISyntaxException e) { - e.printStackTrace(); + logError("", e); } } diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/ChoiceScoreController.java b/src/main/java/org/olat/ims/qti21/ui/editor/ChoiceScoreController.java index bda5a920bfa048c41f1474072fd088ad7d8ae61e..c3bbc66c4c4186359d0e1f65983167700e02e4ab 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/ChoiceScoreController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/ChoiceScoreController.java @@ -31,6 +31,7 @@ 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.util.Formatter; +import org.olat.core.util.StringHelper; import org.olat.core.util.filter.FilterFactory; import org.olat.ims.qti21.model.xml.AssessmentHtmlBuilder; import org.olat.ims.qti21.model.xml.ScoreBuilder; @@ -112,6 +113,40 @@ public class ChoiceScoreController extends FormBasicController { formLayout.add(buttonsContainer); uifactory.addFormSubmitButton("submit", buttonsContainer); } + + + + @Override + protected boolean validateFormLogic(UserRequest ureq) { + boolean allOk = true; + allOk &= validateDouble(maxScoreEl); + + if(assessmentModeEl.isOneSelected() && assessmentModeEl.isSelected(1)) { + for(SimpleChoiceWrapper wrapper:wrappers) { + allOk &= validateDouble(wrapper.getPointsEl()); + } + } + + return allOk & super.validateFormLogic(ureq); + } + + private boolean validateDouble(TextElement el) { + boolean allOk = true; + + String value = el.getValue(); + if(!StringHelper.containsNonWhitespace(value)) { + el.setErrorKey("form.legende.mandatory", null); + allOk &= false; + } else { + try { + Double.parseDouble(value); + } catch (NumberFormatException e) { + el.setErrorKey("error.double", null); + allOk &= false; + } + } + return allOk; + } @Override protected void formOK(UserRequest ureq) { diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_content/choices_score.html b/src/main/java/org/olat/ims/qti21/ui/editor/_content/choices_score.html index 4d8758a349fecbf2eb5a8f1d03baebb68b1c3043..69ac6be1581b417291ed9ae1a91787239ee3f966 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_content/choices_score.html +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_content/choices_score.html @@ -9,7 +9,11 @@ <tr> <td>#if(${choice.isCorrect()}) <i class="o_icon o_icon-lg o_icon_check_on"> </i> #end</td> <td>$choice.summary</td> - <td>$r.render($choice.getPointsEl().getComponent().getComponentName())</td> + <td>$r.render($choice.getPointsEl().getComponent().getComponentName()) + #if($f.hasError($choice.getPointsEl().getComponent().getComponentName())) + <div>$r.render("${choice.getPointsEl().getComponent().getComponentName()}_ERROR")</div> + #end + </td> </tr> #end </tbody> 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 c29aee629b5f4219e4259d60cd45f9c2db1a38a7..ea8bdde7f273d2cb4f511b7b9540d8a89aac1cda 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 @@ -4,6 +4,9 @@ editor.sc.title=Single choice editor.unkown.title=Unkown interaction error.cannot.create.section=A section cannot be created everywhere! error.integer=Need to be an integer +error.double=Need to be a double +error.need.correct.answer=Need a least a correct answer +error.import.question=An unexpected error during import of a question essay.columns=Width (number of letters per line) essay.min.strings=Min. words essay.max.strings=Max. words @@ -55,3 +58,7 @@ new.mc=Multiple choice new.kprim=KPrim new.section=Section time.limit.max=Time limit (minute) +title.add=$org.olat.ims.qti.editor\:title.add +tools.import.qpool=$org.olat.ims.qti.editor\:tools.import.qpool +tools.import.table=$org.olat.ims.qti.editor\:tools.import.table + diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java index 141e8e96b9fa52004f903668f27ac784ce61411c..42d125fb34891cab2562dfc31666bc910bb3ab9a 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MultipleChoiceEditorController.java @@ -177,6 +177,12 @@ public class MultipleChoiceEditorController extends FormBasicController { allOk &= false; } + String[] correctAnswers = ureq.getHttpReq().getParameterValues("correct"); + if(correctAnswers == null || correctAnswers.length == 0) { + answersCont.setErrorKey("error.need.correct.answer", null); + allOk &= false; + } + return allOk & super.validateFormLogic(ureq); } 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 cd29a5b65c9318f10fa9a535222d8a22d1036f27..7925d6d10f6cd79b9388a10549ba71811dd87448 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 @@ -178,7 +178,7 @@ public class SingleChoiceEditorController extends FormBasicController { String correctAnswer = ureq.getParameter("correct"); if(!StringHelper.containsNonWhitespace(correctAnswer)) { allOk &= false; - textEl.setErrorKey("form.legende.mandatory", null); + answersCont.setErrorKey("error.need.correct.answer", null); } return allOk & super.validateFormLogic(ureq); diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/simple_choices_editor.html b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/simple_choices_editor.html index 5245149a4f1b36645b6faa6ae16825dba831d807..1cec94ea7ee0612c7925d0a5c2889f8d04db26d1 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/simple_choices_editor.html +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/simple_choices_editor.html @@ -1,3 +1,6 @@ $r.render("metadata") +#if($f.hasError("answers")) + <div>$r.render("answers_ERROR")</div> +#end $r.render("answers") $r.render("buttons") \ No newline at end of file