diff --git a/pom.xml b/pom.xml index 56c4b16883c5de8ce7be538f7a5689579e7194f8..efbded5bf8e481802d87cf9aafc9317a6d01ab91 100644 --- a/pom.xml +++ b/pom.xml @@ -2087,7 +2087,7 @@ <dependency> <groupId>org.openolat.imscp</groupId> <artifactId>manifest</artifactId> - <version>1.3.0</version> + <version>1.4.1</version> </dependency> <dependency> <groupId>com.rometools</groupId> 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 index af88a7f3a713f806f4706612dafbe8a537e64d00..e4b529267b07240cd179b535be510af91505d2cf 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemMetadata.java @@ -232,16 +232,16 @@ public class AssessmentItemMetadata { //openolat metadata if(differentiation != null) { - metadata.setOpenOLATMetadataMasterDiscriminationIndex(differentiation.doubleValue()); + metadata.setOpenOLATMetadataDiscriminationIndex(differentiation.doubleValue()); } if(difficulty != null) { - metadata.setOpenOLATMetadataMasterDifficulty(difficulty.doubleValue()); + metadata.setOpenOLATMetadataDifficulty(difficulty.doubleValue()); } if(stdevDifficulty != null) { - metadata.setOpenOLATMetadataMasterStandardDeviation(stdevDifficulty.doubleValue()); + metadata.setOpenOLATMetadataStandardDeviation(stdevDifficulty.doubleValue()); } if(numOfAnswerAlternatives >= 0) { - metadata.setOpenOLATMetadataMasterDistractors(numOfAnswerAlternatives); + metadata.setOpenOLATMetadataDistractors(numOfAnswerAlternatives); } } diff --git a/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java index 01e478957b407ea7132a4a4c8fdd5f9d4ecd4599..53141654617a6124f1ee22558df3daadc48d9c03 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/ManifestMetadataBuilder.java @@ -19,13 +19,22 @@ */ package org.olat.ims.qti21.model.xml; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.StringTokenizer; import javax.xml.bind.JAXBElement; +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; import javax.xml.namespace.QName; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.imscp.xml.manifest.ManifestMetadataType; import org.olat.imscp.xml.manifest.MetadataType; @@ -44,6 +53,7 @@ import org.olat.imsmd.xml.manifest.LomType; import org.olat.imsmd.xml.manifest.PurposeType; import org.olat.imsmd.xml.manifest.RightsType; import org.olat.imsmd.xml.manifest.SourceType; +import org.olat.imsmd.xml.manifest.StatusType; import org.olat.imsmd.xml.manifest.StringType; import org.olat.imsmd.xml.manifest.TaxonType; import org.olat.imsmd.xml.manifest.TaxonpathType; @@ -53,8 +63,14 @@ import org.olat.imsmd.xml.manifest.TypicallearningtimeType; import org.olat.imsmd.xml.manifest.ValueType; import org.olat.imsmd.xml.manifest.VersionType; import org.olat.imsqti.xml.manifest.QTIMetadataType; +import org.olat.modules.qpool.QuestionItem; +import org.olat.modules.qpool.model.QLicense; import org.olat.oo.xml.manifest.OpenOLATMetadataType; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; +import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; + /** * * Initial date: 23.02.2016<br> @@ -63,6 +79,8 @@ import org.olat.oo.xml.manifest.OpenOLATMetadataType; */ public class ManifestMetadataBuilder { + private static final OLog log = Tracing.createLoggerFor(ManifestMetadataBuilder.class); + protected static final org.olat.oo.xml.manifest.ObjectFactory ooObjectFactory = new org.olat.oo.xml.manifest.ObjectFactory(); protected static final org.olat.imscp.xml.manifest.ObjectFactory cpObjectFactory = new org.olat.imscp.xml.manifest.ObjectFactory(); protected static final org.olat.imsmd.xml.manifest.ObjectFactory mdObjectFactory = new org.olat.imsmd.xml.manifest.ObjectFactory(); @@ -92,6 +110,18 @@ public class ManifestMetadataBuilder { this.metadata = metadata; } + public String getIdentifier() { + GeneralType general = getGeneral(false); + if(general != null) { + for(Object any:general.getContent()) { + if(any instanceof JAXBElement<?> && ((JAXBElement<?>)any).getName().getLocalPart().equals("identifier")) { + return (String)((JAXBElement<?>)any).getValue(); + } + } + } + return null; + } + public String getTitle() { GeneralType general = getGeneral(false); if(general != null) { @@ -183,6 +213,17 @@ public class ManifestMetadataBuilder { } } + public String getCoverage() { + GeneralType general = getGeneral(false); + if(general != null) { + CoverageType type = getFromAny(CoverageType.class, general.getContent()); + if(type != null) { + return getFirstString(type.getLangstring()); + } + } + return null; + } + public void setCoverage(String coverage, String lang) { GeneralType general = getGeneral(true); if(general != null) { @@ -239,6 +280,17 @@ public class ManifestMetadataBuilder { } } + public String getEducationalLearningTime() { + EducationalType educational = getEducational(true); + if(educational != null) { + TypicallearningtimeType type = getFromAny(TypicallearningtimeType.class, educational.getContent()); + if(type != null) { + return type.getDatetime(); + } + } + return null; + } + public void setEducationalLearningTime(String datetime) { EducationalType educational = getEducational(true); if(educational != null) { @@ -431,16 +483,36 @@ public class ManifestMetadataBuilder { return educational; } + public String getLifecycleVersion() { + LifecycleType lifecycle = getLifecycle(false); + if(lifecycle != null) { + VersionType type = getFromAny(VersionType.class, lifecycle.getContent()); + if(type != null) { + return getFirstString(type.getLangstring()); + } + } + return null; + } + public void setLifecycleVersion(String version) { LifecycleType lifecycle = getLifecycle(true); + VersionType type = getFromAny(VersionType.class, lifecycle.getContent()); + if(type == null) { + type = mdObjectFactory.createVersionType(); + lifecycle.getContent().add(mdObjectFactory.createVersion(type)); + } + createOrUpdateFirstLangstring(type.getLangstring(), version, "en"); + } + + public String getLifecycleStatus() { + LifecycleType lifecycle = getLifecycle(false); if(lifecycle != null) { - VersionType type = getFromAny(VersionType.class, lifecycle.getContent()); - if(type == null) { - type = mdObjectFactory.createVersionType(); - lifecycle.getContent().add(mdObjectFactory.createVersion(type)); + StatusType status = getFromAny(StatusType.class, lifecycle.getContent()); + if(status != null && status.getValue() != null && status.getValue().getLangstring() != null) { + return status.getValue().getLangstring().getValue(); } - createOrUpdateFirstLangstring(type.getLangstring(), version, "en"); } + return null; } public LifecycleType getLifecycle(boolean create) { @@ -489,44 +561,127 @@ public class ManifestMetadataBuilder { return ooMetadata; } + public String getOpenOLATMetadataQuestionType() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getQuestionType(); + } + public void setOpenOLATMetadataQuestionType(String questionType) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setQuestionType(questionType); + getOpenOLATMetadata(true).setQuestionType(questionType); + } + + public String getOpenOLATMetadataIdentifier() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getQpoolIdentifier(); + } + + public void setOpenOLATMetadataIdentifier(String identifier) { + getOpenOLATMetadata(true).setQpoolIdentifier(identifier); + } + + public String getOpenOLATMetadataMasterIdentifier() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getMasterIdentifier(); } public void setOpenOLATMetadataMasterIdentifier(String masterIdentifier) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setMasterIdentifier(masterIdentifier); + getOpenOLATMetadata(true).setMasterIdentifier(masterIdentifier); + } + + public Double getOpenOLATMetadataDifficulty() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getDifficulty(); + } + + public void setOpenOLATMetadataDifficulty(Double difficulty) { + getOpenOLATMetadata(true).setDifficulty(difficulty); } - public void setOpenOLATMetadataMasterDifficulty(Double difficulty) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setDifficulty(difficulty); + public void setOpenOLATMetadataDifficulty(BigDecimal difficulty) { + if(difficulty == null) { + setOpenOLATMetadataDifficulty((Double)null); + } else { + setOpenOLATMetadataDifficulty(Double.valueOf(difficulty.doubleValue())); + } + } + + public Double getOpenOLATMetadataDiscriminationIndex() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getDiscriminationIndex(); + } + + public void setOpenOLATMetadataDiscriminationIndex(Double discriminationIndex) { + getOpenOLATMetadata(true).setDiscriminationIndex(discriminationIndex); + } + + public void setOpenOLATMetadataDiscriminationIndex(BigDecimal discriminationIndex) { + if(discriminationIndex == null) { + setOpenOLATMetadataDiscriminationIndex((Double)null); + } else { + setOpenOLATMetadataDiscriminationIndex(Double.valueOf(discriminationIndex.doubleValue())); + } + } + + public Integer getOpenOLATMetadataDistractors() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getDistractors(); + } + + public void setOpenOLATMetadataDistractors(Integer distractors) { + getOpenOLATMetadata(true).setDistractors(distractors); } - public void setOpenOLATMetadataMasterDiscriminationIndex(Double discriminationIndex) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setDiscriminationIndex(discriminationIndex); + public Double getOpenOLATMetadataStandardDeviation() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getStandardDeviation(); } - public void setOpenOLATMetadataMasterDistractors(Integer distractors) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setDistractors(distractors); + public void setOpenOLATMetadataStandardDeviation(Double standardDeviation) { + getOpenOLATMetadata(true).setStandardDeviation(standardDeviation); } - public void setOpenOLATMetadataMasterStandardDeviation(Double standardDeviation) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setStandardDeviation(standardDeviation); + public void setOpenOLATMetadataStandardDeviation(BigDecimal standardDeviation) { + if(standardDeviation == null) { + setOpenOLATMetadataStandardDeviation((Double)null); + } else { + setOpenOLATMetadataStandardDeviation(Double.valueOf(standardDeviation.doubleValue())); + } + } + + public Integer getOpenOLATMetadataUsage() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getUsage(); } public void setOpenOLATMetadataUsage(Integer usage) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setUsage(usage); + getOpenOLATMetadata(true).setUsage(usage); + } + + public String getOpenOLATMetadataAssessmentType() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + return ooMetadata == null ? null : ooMetadata.getAssessmentType(); } public void setOpenOLATMetadataAssessmentType(String type) { - OpenOLATMetadataType qtiMetadata = getOpenOLATMetadata(true); - qtiMetadata.setAssessmentType(type); + getOpenOLATMetadata(true).setAssessmentType(type); + } + + public Date getOpenOLATMetadataCopiedAt() { + OpenOLATMetadataType ooMetadata = getOpenOLATMetadata(false); + if(ooMetadata != null && ooMetadata.getCopiedAt() != null) { + return ooMetadata.getCopiedAt().toGregorianCalendar().getTime(); + } + return null; + } + + public void setOpenOLATMetadataCopiedAt(Date date) { + try { + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(date); + getOpenOLATMetadata(true).setCopiedAt(DatatypeFactory.newInstance().newXMLGregorianCalendar(cal)); + } catch (DatatypeConfigurationException e) { + log.error("", e); + } } /** @@ -554,6 +709,21 @@ public class ManifestMetadataBuilder { return qtiMetadata; } + public String getQtiMetadataToolName() { + QTIMetadataType qtiMetadata = getQtiMetadata(false); + return qtiMetadata == null ? null : qtiMetadata.getToolName(); + } + + public String getQtiMetadaToolVendor() { + QTIMetadataType qtiMetadata = getQtiMetadata(false); + return qtiMetadata == null ? null : qtiMetadata.getToolVendor(); + } + + public String getQtiMetadataToolVersion() { + QTIMetadataType qtiMetadata = getQtiMetadata(false); + return qtiMetadata == null ? null : qtiMetadata.getToolVersion(); + } + public void setQtiMetadataTool(String toolName, String toolVendor, String toolVersion) { QTIMetadataType qtiMetadata = getQtiMetadata(true); qtiMetadata.setToolName(toolName); @@ -561,7 +731,7 @@ public class ManifestMetadataBuilder { qtiMetadata.setToolVersion(toolVersion); } - public void setQtiMetadata(List<String> interactions) { + public void setQtiMetadataInteractionTypes(List<String> interactions) { QTIMetadataType qtiMetadata = getQtiMetadata(true); qtiMetadata.getInteractionType().clear(); @@ -605,5 +775,94 @@ public class ManifestMetadataBuilder { public List<Object> getMetadataList() { return metadata == null ? manifestMetadata.getAny() : metadata.getAny(); } + + public void appendMetadataFrom(QuestionItem item, ResolvedAssessmentItem resolvedAssessmentItem, Locale locale) { + AssessmentItem assessmentItem = null; + if(resolvedAssessmentItem != null) { + assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); + } + appendMetadataFrom(item, assessmentItem, locale); + } + + /** + * This method will add new metadata to the current ones. + * + * @param item + * @param locale + */ + public void appendMetadataFrom(QuestionItem item, AssessmentItem assessmentItem, Locale locale) { + String lang = item.getLanguage(); + if(!StringHelper.containsNonWhitespace(lang)) { + lang = locale.getLanguage(); + } + //LOM : General + if(StringHelper.containsNonWhitespace(item.getTitle())) { + setTitle(item.getTitle(), lang); + } + if(StringHelper.containsNonWhitespace(item.getCoverage())) { + setCoverage(item.getCoverage(), lang); + } + if(StringHelper.containsNonWhitespace(item.getKeywords())) { + setGeneralKeywords(item.getKeywords(), lang); + } + if(StringHelper.containsNonWhitespace(item.getDescription())) { + setDescription(item.getDescription(), lang); + } + //LOM : Technical + setTechnicalFormat(ManifestBuilder.ASSESSMENTITEM_MIMETYPE); + //LOM : Educational + if(StringHelper.containsNonWhitespace(item.getEducationalContextLevel())) { + setEducationalContext(item.getEducationalContextLevel(), lang); + } + if(StringHelper.containsNonWhitespace(item.getEducationalLearningTime())) { + setEducationalLearningTime(item.getEducationalLearningTime()); + } + if(item.getLanguage() != null) { + setLanguage(item.getLanguage(), lang); + } + //LOM : Lifecycle + if(StringHelper.containsNonWhitespace(item.getItemVersion())) { + setLifecycleVersion(item.getItemVersion()); + } + //LOM : Rights + QLicense license = item.getLicense(); + if(license != null) { + if(StringHelper.containsNonWhitespace(license.getLicenseText())) { + setLicense(license.getLicenseText()); + } else if(StringHelper.containsNonWhitespace(license.getLicenseKey())) { + setLicense(license.getLicenseKey()); + } + } + //LOM : classification + if(StringHelper.containsNonWhitespace(item.getTaxonomicPath())) { + setClassificationTaxonomy(item.getTaxonomicPath(), lang); + } + + // QTI 2.1 + setQtiMetadataTool(item.getEditor(), null, item.getEditorVersion()); + + if(assessmentItem != null) { + List<Interaction> interactions = assessmentItem.getItemBody().findInteractions(); + List<String> interactionNames = new ArrayList<>(interactions.size()); + for(Interaction interaction:interactions) { + interactionNames.add(interaction.getQtiClassName()); + } + setQtiMetadataInteractionTypes(interactionNames); + } + + // OpenOLAT + if(item.getType() != null) { + setOpenOLATMetadataQuestionType(item.getType().getType()); + } else { + setOpenOLATMetadataQuestionType(null); + } + setOpenOLATMetadataIdentifier(item.getIdentifier()); + setOpenOLATMetadataDifficulty(item.getDifficulty()); + setOpenOLATMetadataDiscriminationIndex(item.getDifferentiation()); + setOpenOLATMetadataDistractors(item.getNumOfAnswerAlternatives()); + setOpenOLATMetadataStandardDeviation(item.getStdevDifficulty()); + setOpenOLATMetadataUsage(item.getUsage()); + setOpenOLATMetadataAssessmentType(item.getAssessmentType()); + } } diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI12To21Converter.java b/src/main/java/org/olat/ims/qti21/pool/QTI12To21Converter.java index b40ae88c1f2daf608622a1489b8fb82190f52eec..9deb03e24ec126d4cc1411488e55aabd835d0599 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI12To21Converter.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI12To21Converter.java @@ -406,7 +406,7 @@ public class QTI12To21Converter { manifest.appendAssessmentItem(itemFile.getName()); ManifestMetadataBuilder metadata = manifest.getResourceBuilderByHref(itemFile.getName()); metadata.setTechnicalFormat(ManifestBuilder.ASSESSMENTITEM_MIMETYPE); - metadata.setQtiMetadata(itemBuilder.getInteractionNames()); + metadata.setQtiMetadataInteractionTypes(itemBuilder.getInteractionNames()); metadata.setOpenOLATMetadataQuestionType(itemBuilder.getQuestionType().getPrefix()); metadata.setTitle(item.getTitle(), locale.getLanguage()); metadata.setDescription(item.getObjectives(), locale.getLanguage()); 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 68c7c8955ee8096e430648df6b5b18bb6f77431f..601f3804d7c40285fb829bb7b6705c23d5c49250 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21ExportProcessor.java @@ -29,7 +29,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.DoubleAdder; @@ -39,7 +38,6 @@ import java.util.zip.ZipOutputStream; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.FileUtils; -import org.olat.core.util.StringHelper; import org.olat.core.util.ZipUtil; import org.olat.core.util.io.ShieldOutputStream; import org.olat.ims.qti21.QTI21Service; @@ -56,7 +54,6 @@ import org.olat.modules.qpool.QuestionItemFull; import org.olat.modules.qpool.manager.QPoolFileStorage; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; -import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; @@ -176,7 +173,7 @@ public class QTI21ExportProcessor { resource = manifestBuilder.appendAssessmentItem(qitem.getRootFilename()); } ManifestMetadataBuilder metadataBuilder = manifestBuilder.getMetadataBuilder(resource, true); - enrichWithMetadata(qitem, resolvedAssessmentItem, metadataBuilder); + metadataBuilder.appendMetadataFrom(qitem, resolvedAssessmentItem, locale); } public void assembleTest(List<QuestionItemFull> fullItems, File directory) { @@ -213,7 +210,7 @@ public class QTI21ExportProcessor { AssessmentTestFactory.appendAssessmentItem(section, newItemFilename); manifest.appendAssessmentItem(newItemFilename); ManifestMetadataBuilder metadata = manifest.getResourceBuilderByHref(newItemFilename); - enrichWithMetadata(qitem, resolvedAssessmentItem, metadata); + metadata.appendMetadataFrom(qitem, resolvedAssessmentItem, locale); Double maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem); if(maxScore != null) { @@ -281,7 +278,7 @@ public class QTI21ExportProcessor { AssessmentTestFactory.appendAssessmentItem(section, containedFilename); manifest.appendAssessmentItem(containedFilename); ManifestMetadataBuilder metadata = manifest.getResourceBuilderByHref(containedFilename); - enrichWithMetadata(qitem, resolvedAssessmentItem, metadata); + metadata.appendMetadataFrom(qitem, resolvedAssessmentItem, locale); //write materials try { @@ -312,94 +309,4 @@ public class QTI21ExportProcessor { log.error("", e); } } - - private void enrichWithMetadata(QuestionItemFull qitem, ResolvedAssessmentItem resolvedAssessmentItem, ManifestMetadataBuilder metadata) { - String lang = qitem.getLanguage(); - if(!StringHelper.containsNonWhitespace(lang)) { - lang = locale.getLanguage(); - } - - //general - if(StringHelper.containsNonWhitespace(qitem.getTitle())) { - metadata.setTitle(qitem.getTitle(), lang); - } - if(StringHelper.containsNonWhitespace(qitem.getDescription())) { - metadata.setDescription(qitem.getDescription(), lang); - } - if(StringHelper.containsNonWhitespace(qitem.getKeywords())) { - //general and classification too - metadata.setGeneralKeywords(qitem.getKeywords(), lang); - } - if(StringHelper.containsNonWhitespace(qitem.getCoverage())) { - metadata.setCoverage(qitem.getCoverage(), lang); - } - - //educational - if(qitem.getEducationalContext() != null) { - String level = qitem.getEducationalContext().getLevel(); - metadata.setEducationalContext(level, lang); - } - if(qitem.getEducationalLearningTime() != null) { - String time = qitem.getEducationalLearningTime(); - metadata.setEducationalLearningTime(time); - } - if(qitem.getLanguage() != null) { - String language = qitem.getLanguage(); - metadata.setLanguage(language, lang); - } - - //classification - if(qitem.getTaxonomicPath() != null) { - metadata.setClassificationTaxonomy(qitem.getTaxonomicPath(), lang); - } - - //life-cycle - if(StringHelper.containsNonWhitespace(qitem.getItemVersion())) { - metadata.setLifecycleVersion(qitem.getItemVersion()); - } - - // rights - if(qitem.getLicense() != null && StringHelper.containsNonWhitespace(qitem.getLicense().getLicenseText())) { - metadata.setLicense(qitem.getLicense().getLicenseText()); - } - - //qti metadata - if(StringHelper.containsNonWhitespace(qitem.getEditor()) || StringHelper.containsNonWhitespace(qitem.getEditorVersion())) { - metadata.setQtiMetadataTool(qitem.getEditor(), null, qitem.getEditorVersion()); - } - - if(resolvedAssessmentItem != null) { - AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); - List<Interaction> interactions = assessmentItem.getItemBody().findInteractions(); - List<String> interactionNames = new ArrayList<>(interactions.size()); - for(Interaction interaction:interactions) { - interactionNames.add(interaction.getQtiClassName()); - } - metadata.setQtiMetadata(interactionNames); - } - - //openolat metadata - metadata.setOpenOLATMetadataQuestionType(qitem.getItemType()); - if(qitem.getAssessmentType() != null) {//summative, formative, both - metadata.setOpenOLATMetadataAssessmentType(qitem.getAssessmentType()); - } - if(qitem.getDifficulty() != null) { - metadata.setOpenOLATMetadataMasterDifficulty(qitem.getDifficulty().doubleValue()); - } - if(qitem.getDifferentiation() != null) { - metadata.setOpenOLATMetadataMasterDiscriminationIndex(qitem.getDifferentiation().doubleValue()); - } - if(qitem.getNumOfAnswerAlternatives() >= 0) { - metadata.setOpenOLATMetadataMasterDistractors(qitem.getNumOfAnswerAlternatives()); - } - if(qitem.getStdevDifficulty() != null) { - metadata.setOpenOLATMetadataMasterStandardDeviation(qitem.getStdevDifficulty().doubleValue()); - } - if(qitem.getUsage() >= 0) { - metadata.setOpenOLATMetadataUsage(qitem.getUsage()); - } - if(StringHelper.containsNonWhitespace(qitem.getMasterIdentifier())) { - metadata.setOpenOLATMetadataMasterIdentifier(qitem.getMasterIdentifier()); - } - } } 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 b247f3d18a3a6b9fdb9601d152475988b0be03a6..877380a91464a28a5ccedb50aa57a36986c95169 100644 --- a/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java +++ b/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java @@ -473,6 +473,10 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { return qitem; } + public QuestionItemFull getFullQuestionItem(QuestionItemShort qitem) { + return questionItemDao.loadById(qitem.getKey()); + } + /** * Export to QTI editor an item from the pool. The ident of the item * is always regenerated as an UUID. @@ -480,10 +484,9 @@ public class QTI21QPoolServiceProvider implements QPoolSPI { * @param editorContainer * @return */ - public AssessmentItem exportToQTIEditor(QuestionItemShort qitem, Locale locale, File editorContainer) throws IOException { + public AssessmentItem exportToQTIEditor(QuestionItemFull qitem, Locale locale, File editorContainer) throws IOException { QTI21ExportProcessor processor = new QTI21ExportProcessor(qtiService, qpoolFileStorage, locale); - QuestionItemFull fullItem = questionItemDao.loadById(qitem.getKey()); - ResolvedAssessmentItem resolvedAssessmentItem = processor.exportToQTIEditor(fullItem, editorContainer); + ResolvedAssessmentItem resolvedAssessmentItem = processor.exportToQTIEditor(qitem, editorContainer); if(resolvedAssessmentItem != null) { AssessmentItem assessmentItem = resolvedAssessmentItem.getItemLookup().extractAssumingSuccessful(); assessmentItem.setIdentifier(QTI21QuestionType.generateNewIdentifier(assessmentItem.getIdentifier())); 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 31632355f0279616c562b445b317ace4333a74ab..c416c02bc87f21efb03583547fe90c8ed7185834 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 @@ -20,6 +20,7 @@ package org.olat.ims.qti21.ui.editor; import java.io.File; +import java.util.List; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; @@ -29,6 +30,7 @@ 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.core.util.StringHelper; import org.olat.core.util.Util; import org.olat.core.util.vfs.VFSContainer; import org.olat.ims.qti21.QTI21Service; @@ -48,6 +50,7 @@ import org.olat.ims.qti21.model.xml.interactions.SingleChoiceAssessmentItemBuild import org.olat.ims.qti21.model.xml.interactions.UploadAssessmentItemBuilder; import org.olat.ims.qti21.ui.AssessmentItemDisplayController; import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent; +import org.olat.ims.qti21.ui.editor.events.DetachFromPoolEvent; import org.olat.ims.qti21.ui.editor.interactions.ChoiceScoreController; import org.olat.ims.qti21.ui.editor.interactions.DrawingEditorController; import org.olat.ims.qti21.ui.editor.interactions.EssayEditorController; @@ -64,6 +67,8 @@ import org.olat.ims.qti21.ui.editor.interactions.SingleChoiceEditorController; import org.olat.ims.qti21.ui.editor.interactions.UploadEditorController; import org.olat.modules.assessment.AssessmentEntry; import org.olat.modules.assessment.AssessmentService; +import org.olat.modules.qpool.QPoolService; +import org.olat.modules.qpool.QuestionItem; import org.olat.repository.RepositoryEntry; import org.springframework.beans.factory.annotation.Autowired; @@ -87,7 +92,7 @@ public class AssessmentItemEditorController extends BasicController { private int displayTabPosition; private int solutionTabPosition; - private MetadataEditorController metadataEditor; + private PoolEditorController poolEditor; private AssessmentItemPreviewController displayCtrl; private Controller itemEditor, scoreEditor, feedbackEditor; private AssessmentItemPreviewSolutionController solutionCtrl; @@ -105,9 +110,10 @@ public class AssessmentItemEditorController extends BasicController { @Autowired private QTI21Service qtiService; @Autowired + private QPoolService qpoolService; + @Autowired private AssessmentService assessmentService; - public AssessmentItemEditorController(UserRequest ureq, WindowControl wControl, ResolvedAssessmentItem resolvedAssessmentItem, File rootDirectory, VFSContainer rootContainer, File itemFile, boolean restrictedEdit, boolean readOnly) { @@ -148,7 +154,7 @@ public class AssessmentItemEditorController extends BasicController { public AssessmentItemEditorController(UserRequest ureq, WindowControl wControl, RepositoryEntry testEntry, ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, ManifestMetadataBuilder metadataBuilder, - File rootDirectory, VFSContainer rootContainer, File itemFile, boolean restrictedEdit, boolean readOnly) { + File rootDirectory, VFSContainer rootContainer, File itemFile, boolean restrictedEdit) { super(ureq, wControl, Util.createPackageTranslator(AssessmentItemDisplayController.class, ureq.getLocale())); this.itemRef = itemRef; this.metadataBuilder = metadataBuilder; @@ -156,7 +162,6 @@ public class AssessmentItemEditorController extends BasicController { this.testEntry = testEntry; this.rootDirectory = rootDirectory; this.rootContainer = rootContainer; - this.readOnly = readOnly; this.restrictedEdit = restrictedEdit; this.resolvedAssessmentItem = resolvedAssessmentItem; @@ -164,6 +169,7 @@ public class AssessmentItemEditorController extends BasicController { || resolvedAssessmentItem.getItemLookup().getRootNodeHolder() == null) { mainVC = createVelocityContainer("missing_resource"); mainVC.contextPut("uri", itemFile == null ? "" : itemFile); + readOnly = true; } else { mainVC = createVelocityContainer("assessment_item_editor"); mainVC.contextPut("restrictedEdit", restrictedEdit); @@ -171,7 +177,15 @@ public class AssessmentItemEditorController extends BasicController { tabbedPane.setElementCssClass("o_sel_assessment_item_config"); tabbedPane.addListener(this); mainVC.put("tabbedpane", tabbedPane); - + + //check the status in the pool + QPoolInformations qStatus = getPoolStatus(); + readOnly = qStatus.isReadOnly(); + if(qStatus.isPooled()) { + poolEditor = new PoolEditorController(ureq, getWindowControl(), itemRef, metadataBuilder, qStatus); + listenTo(poolEditor); + } + initItemEditor(ureq); AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(getIdentity(), null, testEntry, null, testEntry); @@ -183,11 +197,46 @@ public class AssessmentItemEditorController extends BasicController { solutionCtrl = new AssessmentItemPreviewSolutionController(ureq, getWindowControl(), resolvedAssessmentItem, rootDirectory, itemFile); listenTo(solutionCtrl); solutionTabPosition = tabbedPane.addTab(translate("preview.solution"), solutionCtrl); + + if(poolEditor != null ) { + tabbedPane.addTab(translate("form.pool"), poolEditor); + } } putInitialPanel(mainVC); } + public QPoolInformations getPoolStatus() { + boolean isReadOnly = false; + boolean pooled = false; + QuestionItem originalItem = null; + QuestionItem masterItem = null; + if(metadataBuilder != null) { + if(StringHelper.containsNonWhitespace(metadataBuilder.getOpenOLATMetadataIdentifier())) { + List<QuestionItem> items = qpoolService.loadItemByIdentifier(metadataBuilder.getOpenOLATMetadataIdentifier()); + if(items.size() > 0) { + pooled = true; + isReadOnly = true; + if(items.size() == 1) { + originalItem = items.get(0); + } + } + } + + if(StringHelper.containsNonWhitespace(metadataBuilder.getOpenOLATMetadataIdentifier())) { + List<QuestionItem> items = qpoolService.loadItemByIdentifier(metadataBuilder.getOpenOLATMetadataMasterIdentifier()); + if(items.size() > 0) { + pooled = true; + if(items.size() == 1) { + masterItem = items.get(0); + } + } + } + + } + return new QPoolInformations(isReadOnly, pooled, originalItem, masterItem); + } + public String getTitle() { return resolvedAssessmentItem.getRootNodeLookup().getRootNodeHolder().getRootNode().getTitle(); } @@ -216,12 +265,6 @@ public class AssessmentItemEditorController extends BasicController { case hottext: itemBuilder = initHottextEditors(ureq, item); break; default: initItemCreatedByUnkownEditor(ureq, item); break; } - - if(metadataBuilder != null) { - //metadataEditor = new MetadataEditorController(ureq, getWindowControl(), metadataBuilder); - //listenTo(metadataEditor); - //tabbedPane.addTab(translate("form.metadata"), metadataEditor); - } return type; } @@ -477,11 +520,9 @@ public class AssessmentItemEditorController extends BasicController { } else if(AssessmentItemEvent.ASSESSMENT_ITEM_NEED_RELOAD.equals(aie.getCommand())) { fireEvent(ureq, event); } - } else if(metadataEditor == source) { - if(event == Event.CHANGED_EVENT) { - doBuildAndCommitMetadata(); - AssessmentItem item = resolvedAssessmentItem.getItemLookup().getRootNodeHolder().getRootNode(); - fireEvent(ureq, new AssessmentItemEvent(AssessmentItemEvent.ASSESSMENT_ITEM_METADATA_CHANGED, item, itemRef, null)); + } else if(poolEditor == source) { + if(event instanceof DetachFromPoolEvent) { + fireEvent(ureq, event); } } super.event(ureq, source, event); @@ -501,7 +542,7 @@ public class AssessmentItemEditorController extends BasicController { //update manifest metadataBuilder.setTechnicalFormat(ManifestBuilder.ASSESSMENTITEM_MIMETYPE); if(itemBuilder != null) { - metadataBuilder.setQtiMetadata(itemBuilder.getInteractionNames()); + metadataBuilder.setQtiMetadataInteractionTypes(itemBuilder.getInteractionNames()); metadataBuilder.setOpenOLATMetadataQuestionType(itemBuilder.getQuestionType().getPrefix()); } else { metadataBuilder.setOpenOLATMetadataQuestionType(QTI21QuestionType.unkown.getPrefix()); 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 369a5ca4a1a2b4c84d9a11eddb706031e1328ba7..a96e2720a569a6973359aa90838f68d41ae16d8a 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 @@ -31,6 +31,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.DoubleAdder; @@ -111,8 +112,10 @@ 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.ims.qti21.ui.editor.events.DetachFromPoolEvent; import org.olat.imscp.xml.manifest.FileType; import org.olat.imscp.xml.manifest.ResourceType; +import org.olat.modules.qpool.QuestionItemFull; import org.olat.modules.qpool.QuestionItemView; import org.olat.modules.qpool.ui.SelectItemController; import org.olat.modules.qpool.ui.events.QItemViewEvent; @@ -443,6 +446,9 @@ public class AssessmentTestComposerController extends MainLayoutBasicController } else if(AssessmentItemEvent.ASSESSMENT_ITEM_NEED_RELOAD.equals(aie.getCommand())) { doReloadItem(ureq); } + } else if(event instanceof DetachFromPoolEvent) { + DetachFromPoolEvent dfpe = (DetachFromPoolEvent)event; + doDetachItemFromPool(ureq, dfpe.getItemRef()); } else if(selectQItemCtrl == source) { cmc.deactivate(); cleanUp(); @@ -739,13 +745,19 @@ public class AssessmentTestComposerController extends MainLayoutBasicController try { AssessmentSection section = (AssessmentSection)sectionNode.getUserObject(); for(QuestionItemView item:items) { - AssessmentItem assessmentItem = qti21QPoolServiceProvider.exportToQTIEditor(item, getLocale(), unzippedDirRoot); + QuestionItemFull qItem = qti21QPoolServiceProvider.getFullQuestionItem(item); + AssessmentItem assessmentItem = qti21QPoolServiceProvider.exportToQTIEditor(qItem, getLocale(), unzippedDirRoot); if(assessmentItem != null) { AssessmentItemRef itemRef = doInsert(section, assessmentItem); if(firstItemId == null) { firstItemId = itemRef.getIdentifier().toString(); } flyingObjects.put(itemRef, assessmentItem); + + ManifestMetadataBuilder metadata = manifestBuilder + .getResourceBuilderByHref(itemRef.getHref().toString()); + metadata.appendMetadataFrom(qItem, assessmentItem, getLocale()); + metadata.setOpenOLATMetadataCopiedAt(new Date()); } else { allOk &= false; } @@ -792,7 +804,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController AssessmentItem assessmentItem = itemBuilder.getAssessmentItem(); AssessmentItemRef itemRef = doInsert(section, assessmentItem); ManifestMetadataBuilder metadata = manifestBuilder.getResourceBuilderByHref(itemRef.getHref().toString()); - metadata.setQtiMetadata(itemBuilder.getInteractionNames()); + metadata.setQtiMetadataInteractionTypes(itemBuilder.getInteractionNames()); itemAndMetadata.toBuilder(metadata, getLocale()); if(firstItemId == null) { firstItemId = itemRef.getIdentifier().toString(); @@ -860,6 +872,15 @@ public class AssessmentTestComposerController extends MainLayoutBasicController return null; } + private TreeNode doDetachItemFromPool(UserRequest ureq, AssessmentItemRef itemRef) { + ManifestMetadataBuilder metadata = manifestBuilder.getResourceBuilderByHref(itemRef.getHref().toString()); + String identifier = metadata.getOpenOLATMetadataIdentifier(); + metadata.setOpenOLATMetadataMasterIdentifier(identifier); + metadata.setOpenOLATMetadataIdentifier(UUID.randomUUID().toString()); + doSaveManifest(); + return doReloadItem(ureq); + } + private TreeNode doReloadItem(UserRequest ureq) { TreeNode selectedNode = menuTree.getSelectedNode(); updateTreeModel(false); @@ -1171,7 +1192,7 @@ public class AssessmentTestComposerController extends MainLayoutBasicController File itemFile = new File(itemUri); ManifestMetadataBuilder metadata = getMetadataBuilder(itemRef); currentEditorCtrl = new AssessmentItemEditorController(ureq, getWindowControl(), testEntry, - item, itemRef, metadata, unzippedDirRoot, unzippedContRoot, itemFile, restrictedEdit, false); + item, itemRef, metadata, unzippedDirRoot, unzippedContRoot, itemFile, restrictedEdit); } } diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/MetadataEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/MetadataEditorController.java deleted file mode 100644 index 46f765e380fc717aef729272a0320ae455a18b6e..0000000000000000000000000000000000000000 --- a/src/main/java/org/olat/ims/qti21/ui/editor/MetadataEditorController.java +++ /dev/null @@ -1,77 +0,0 @@ -/** - * <a href="http://www.openolat.org"> - * OpenOLAT - Online Learning and Training</a><br> - * <p> - * Licensed under the Apache License, Version 2.0 (the "License"); <br> - * you may not use this file except in compliance with the License.<br> - * You may obtain a copy of the License at the - * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> - * <p> - * Unless required by applicable law or agreed to in writing,<br> - * software distributed under the License is distributed on an "AS IS" BASIS, <br> - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> - * See the License for the specific language governing permissions and <br> - * limitations under the License. - * <p> - * Initial code contributed and copyrighted by<br> - * frentix GmbH, http://www.frentix.com - * <p> - */ -package org.olat.ims.qti21.ui.editor; - -import org.olat.core.gui.UserRequest; -import org.olat.core.gui.components.form.flexible.FormItemContainer; -import org.olat.core.gui.components.form.flexible.elements.TextElement; -import org.olat.core.gui.components.form.flexible.impl.FormBasicController; -import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; -import org.olat.core.gui.control.Controller; -import org.olat.core.gui.control.Event; -import org.olat.core.gui.control.WindowControl; -import org.olat.ims.qti21.model.xml.ManifestMetadataBuilder; - -/** - * - * Initial date: 23.02.2016<br> - * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com - * - */ -public class MetadataEditorController extends FormBasicController { - - private TextElement titleEl, descriptionEl; - //keywords, language - - private final ManifestMetadataBuilder metadataBuilder; - - public MetadataEditorController(UserRequest ureq, WindowControl wControl, ManifestMetadataBuilder metadataBuilder) { - super(ureq, wControl); - this.metadataBuilder = metadataBuilder; - initForm(ureq); - } - - @Override - protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { - String title = metadataBuilder.getTitle(); - titleEl = uifactory.addTextElement("title", "form.metadata.title", 256, title, formLayout); - - String description = metadataBuilder.getDescription(); - descriptionEl = uifactory.addTextAreaElement("description", "form.metadata.description", 4000, 4, 60, true, description, formLayout); - - // Submit Button - FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); - buttonsContainer.setRootForm(mainForm); - formLayout.add(buttonsContainer); - uifactory.addFormSubmitButton("submit", buttonsContainer); - } - - @Override - protected void doDispose() { - // - } - - @Override - protected void formOK(UserRequest ureq) { - metadataBuilder.setTitle(titleEl.getValue(), "en"); - metadataBuilder.setDescription(descriptionEl.getValue(), "en"); - fireEvent(ureq, Event.CHANGED_EVENT); - } -} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/PoolEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/PoolEditorController.java new file mode 100644 index 0000000000000000000000000000000000000000..dfbc2fcb16552f1eafcd94c1d5be07d2f6e08a90 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/PoolEditorController.java @@ -0,0 +1,155 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.ui.editor; + +import java.util.Date; +import java.util.List; + +import org.olat.NewControllerFactory; +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItem; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.FormLink; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormEvent; +import org.olat.core.gui.components.link.Link; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.id.Identity; +import org.olat.core.util.Formatter; +import org.olat.ims.qti21.model.xml.ManifestMetadataBuilder; +import org.olat.ims.qti21.ui.editor.events.DetachFromPoolEvent; +import org.olat.modules.qpool.QPoolService; +import org.olat.modules.qpool.QuestionItem; +import org.olat.user.UserManager; +import org.springframework.beans.factory.annotation.Autowired; + +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; + +/** + * + * Initial date: 23.02.2016<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class PoolEditorController extends FormBasicController { + + private FormLink identifierLink; + private FormLink masterIdentifierLink; + private FormLink copyButton; + + private final QuestionItem originalItem; + private final QuestionItem masterItem; + private final AssessmentItemRef itemRef; + private final ManifestMetadataBuilder metadataBuilder; + + @Autowired + private UserManager userManager; + @Autowired + private QPoolService qpoolService; + + public PoolEditorController(UserRequest ureq, WindowControl wControl, + AssessmentItemRef itemRef, ManifestMetadataBuilder metadataBuilder, QPoolInformations poolInformations) { + super(ureq, wControl); + this.itemRef = itemRef; + originalItem = poolInformations.getOriginalItem(); + masterItem = poolInformations.getMasterItem(); + this.metadataBuilder = metadataBuilder; + initForm(ureq); + } + + @Override + protected void doDispose() { + // + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + boolean copy = originalItem == null && masterItem != null; + if(copy) { + setFormWarning("warning.copy.from.pool"); + } + + String identifier = metadataBuilder.getOpenOLATMetadataIdentifier(); + if(originalItem == null) { + uifactory.addStaticTextElement("general.identifier", identifier, formLayout); + } else { + identifierLink = uifactory.addFormLink("general.identifier", identifier, translate("general.identifier"), formLayout, Link.NONTRANSLATED); + } + String masterIdentifier = metadataBuilder.getOpenOLATMetadataMasterIdentifier(); + if(masterItem == null) { + uifactory.addStaticTextElement("general.master.identifier", masterIdentifier, formLayout); + } else { + masterIdentifierLink = uifactory.addFormLink("general.master.identifier", masterIdentifier, translate("general.master.identifier"), formLayout, Link.NONTRANSLATED); + } + + //Rights owners + List<Identity> authors = null; + if(originalItem != null) { + authors = qpoolService.getAuthors(originalItem); + } else if(masterItem != null) { + authors = qpoolService.getAuthors(masterItem); + } + if(authors != null && !authors.isEmpty()) { + String author = userManager.getUserDisplayName(authors.get(0)); + uifactory.addStaticTextElement("rights.owners", author, formLayout); + for(int i=1; i<authors.size(); i++) { + author = userManager.getUserDisplayName(authors.get(i)); + uifactory.addStaticTextElement("rightss.owners_" + i, null, author, formLayout); + } + } + + Date copiedAt = metadataBuilder.getOpenOLATMetadataCopiedAt(); + String copiedAtStr = Formatter.getInstance(getLocale()).formatDateAndTime(copiedAt); + uifactory.addStaticTextElement("copy.at", copiedAtStr, formLayout); + + String version = metadataBuilder.getLifecycleVersion(); + uifactory.addStaticTextElement("lifecycle.version", version, formLayout); + + copyButton = uifactory.addFormLink("copy.qpool.question", formLayout, Link.BUTTON); + copyButton.setVisible(!copy); + } + + @Override + protected void formOK(UserRequest ureq) { + // + } + + @Override + protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { + if(identifierLink == source) { + doOpenQuestion(ureq, originalItem); + } else if(masterIdentifierLink == source) { + doOpenQuestion(ureq, masterItem); + } else if(copyButton == source) { + doCopy(ureq); + } + super.formInnerEvent(ureq, source, event); + } + + private void doOpenQuestion(UserRequest ureq, QuestionItem item) { + String businessPath = "[QPool:0][QuestionItem:" + item.getKey() + "]"; + NewControllerFactory.getInstance().launch(businessPath, ureq, getWindowControl()); + } + + private void doCopy(UserRequest ureq) { + fireEvent(ureq, new DetachFromPoolEvent(itemRef)); + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/QPoolInformations.java b/src/main/java/org/olat/ims/qti21/ui/editor/QPoolInformations.java new file mode 100644 index 0000000000000000000000000000000000000000..b8545240e71f30debed076b39738a59d193d2be8 --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/QPoolInformations.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.ui.editor; + +import org.olat.modules.qpool.QuestionItem; + +/** + * + * Initial date: 9 janv. 2018<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QPoolInformations { + + private final boolean readOnly; + private final boolean pooled; + private final QuestionItem originalItem; + private final QuestionItem masterItem; + + public QPoolInformations(boolean readOnly, boolean pooled, QuestionItem originalItem, QuestionItem masterItem) { + this.readOnly = readOnly; + this.pooled = pooled; + this.originalItem = originalItem; + this.masterItem = masterItem; + } + + public boolean isReadOnly() { + return readOnly; + } + + public boolean isPooled() { + return pooled; + } + + public QuestionItem getOriginalItem() { + return originalItem; + } + + public QuestionItem getMasterItem() { + return masterItem; + } +} diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties index 712cd3d839f302b54d90a89523700f82c7ed09a7..7265ffff0ecb41eb0ec655aa42c15e0518af84af 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties @@ -17,6 +17,8 @@ change.elements=Elemente \u00E4ndern convert=Konvertieren convert.alien=Konvertieren convert.to=Umwandeln in\: +copy.qpool.question=Kopie erstellen und bearbeiten +copy.at=Kopiert am correct.answers=Korrekt cut.value=Notwendige Punktzahl f\u00FCr "Bestanden" delete=L\u00F6schen @@ -134,6 +136,7 @@ form.match=Match form.metadata=Metadaten form.metadata.description=Beschreibung form.metadata.title=Titel +form.pool=\u00DCbertrag form.score=Punkte form.score.answer.correct=Korrekt form.score.answer.points=Punkte @@ -156,6 +159,8 @@ form.testPart.navigationMode.linear=Linear form.testPart.navigationMode.nonlinear=Nicht linear form.unkown=Unbekannt form.upload=Datei hochladen +general.identifier=$org.olat.modules.qpool.ui\:general.identifier +general.master.identifier=$org.olat.modules.qpool.ui\:general.master.identifier hotspot.layout=Hotspot Farbe hotspot.layout.green=Gr\u00FCn hotspot.layout.inverted=Invertiert @@ -175,6 +180,7 @@ item.session.control.attempts=Versuch item.session.control.attempts.hint=Diese Einschr\u00E4nkung der L\u00F6sungsversuche bezieht sich auf einen Test-Part und nicht den gesamten Test. Die Versuche f\u00FCr den gesamten Test k\u00F6nnen unter "Optionen" eingeschr\u00E4nkt werden. item.session.control.show.solution=L\u00F6sung anzeigen item.session.control.show.solution.hint=Beim R\u00FCckblick werden auch L\u00F6sungen angezeigt. +lifecycle.version=$org.olat.modules.qpool.ui\:lifecycle.version math.operator.bigger=> math.operator.biggerEquals=>\= math.operator.equals=\= @@ -211,6 +217,7 @@ new.testpart=Test-Part new.upload=Datei hochladen preview=Vorschau preview.solution=Vorschau L\u00F6sung +rights.owners=$org.olat.modules.qpool.ui\:rights.owners time.limit.max=Zeitbeschr\u00E4nkung title.add=$org.olat.ims.qti.editor\:title.add tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy @@ -232,6 +239,7 @@ warning.atleastone=Bitte w\u00E4hlen Sie mindestens ein Element. warning.atleastonesection=Diese Sektion kann nicht gel\u00F6scht werden. Ein Test oder ein Test-Part muss mindestens eine Sektion enthalten. warning.conversion.list=Es wurde Inkompatibilit\u00E4ten gefunde\: warning.conversion.standard=Es gibt ein Risiko dass sie Daten verlieren obwohl kein Inkompatibilit\u00E4t entdeckt wurde. +warning.copy.from.pool=Die Frage wurde von Fragenpool kopiert. Sie k\u00F6nnen den Original Frage unter Master ID finden. warning.custom.operator=Es wurde ein Hersteller Spezifisch Extension gefunden. Diese ist nicht von Editor unterst\u00FCtzt. warning.feedback.cutvalue=Feedback wird aktiviert sobald bei "Notwendige Punktzahl f\u00FCr 'Bestanden'" eine Punktzahl eingegeben wurde. warning.in.use=Die Ressource wird bereits f\u00FCr Auswertung verwendet. Die Bearbeitung ist begrenzt. 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 781191404731679692b1838a99e2e73bb77dc11b..dba3725a4934781934161295978ecadae1284e48 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 @@ -16,6 +16,8 @@ convert=Convert convert.alien=Convert correct.answers=Correct convert.to=Convert to: +copy.qpool.question=make a copy and edit +copy.at=Copied at cut.value=Cut value delete=Delete delete.item=$org.olat.ims.qti.editor\:delete.item @@ -133,6 +135,7 @@ form.match=Match form.metadata=Metadata form.metadata.description=Description form.metadata.title=Title +form.pool=Transfer form.score=Score form.score.answer.correct=Correct form.score.answer.points=Points @@ -155,6 +158,8 @@ form.testPart.navigationMode.linear=Linear form.testPart.navigationMode.nonlinear=Non linear form.unkown=Unkown form.upload=File upload +general.identifier=$org.olat.modules.qpool.ui\:general.identifier +general.master.identifier=$org.olat.modules.qpool.ui\:general.master.identifier hotspot.layout=Layout hotspot.layout.standard=Standard blue hotspot.layout.inverted=Inverted @@ -174,6 +179,7 @@ item.session.control.attempts=Attempts item.session.control.attempts.hint=This limitation of the attempts is only valid for the parts not for the whole test. The attempts for the whole test can be limited in "Options". item.session.control.show.solution=Show solution item.session.control.show.solution.hint=Solution is shown in review as well. +lifecycle.version=$org.olat.modules.qpool.ui\:lifecycle.version math.operator.bigger=> math.operator.biggerEquals=>\= math.operator.equals=\= @@ -211,6 +217,7 @@ new.testpart=Test part new.upload=Upload file preview=Preview preview.solution=Preview solution +rights.owners=$org.olat.modules.qpool.ui\:rights.owners time.limit.max=Time limit title.add=$org.olat.ims.qti.editor\:title.add SINGLE=Single choice @@ -231,6 +238,7 @@ warning.alien.assessment.test=This test cannot be processed with the OpenOLAT ed warning.alien.feedbacks=This question contains feedbacks which are not compatible with this editor. They will be lost after conversion. warning.atleastone=Please, choose at least one element. warning.atleastonesection=The section cannot be deleted. A test or a test part must contain at least one section. +warning.copy.from.pool=The question was copied from the question bank. You can found the original question under the master id. warning.conversion.list=Some incompatibilities was found\: warning.conversion.standard=There is a risk that some data got lost after conversion but no flagrant incompatibilities was found. warning.custom.operator=This question contains creator specific extension which are currently not supported by OpenOLAT. diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/events/DetachFromPoolEvent.java b/src/main/java/org/olat/ims/qti21/ui/editor/events/DetachFromPoolEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..d97c78e4a13b1dc25b5d25acb890d811d6a7cbab --- /dev/null +++ b/src/main/java/org/olat/ims/qti21/ui/editor/events/DetachFromPoolEvent.java @@ -0,0 +1,47 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.ims.qti21.ui.editor.events; + +import org.olat.core.gui.control.Event; + +import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; + +/** + * + * Initial date: 9 janv. 2018<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class DetachFromPoolEvent extends Event { + + private static final long serialVersionUID = -5881430629517600411L; + public static final String DETACH_FROM_POOL = "detach-from-pool"; + + private final AssessmentItemRef itemRef; + + public DetachFromPoolEvent(AssessmentItemRef itemRef) { + super(DETACH_FROM_POOL); + this.itemRef = itemRef; + } + + public AssessmentItemRef getItemRef() { + return itemRef; + } +} diff --git a/src/main/java/org/olat/modules/qpool/QPoolService.java b/src/main/java/org/olat/modules/qpool/QPoolService.java index 08f0936f99ea63417fc23f0f6d4e9ff1cee7ce67..af74432ca5807981bfbed3217cd5936cface0db8 100644 --- a/src/main/java/org/olat/modules/qpool/QPoolService.java +++ b/src/main/java/org/olat/modules/qpool/QPoolService.java @@ -68,8 +68,18 @@ public interface QPoolService { public QuestionItem createAndPersistItem(Identity owner, String subject, String format, String language, TaxonomyLevel taxonLevel, String dir, String rootFilename, QItemType type); + /** + * @param key The primary key + * @return The question item or null if not found + */ public QuestionItem loadItemById(Long key); + /** + * @param identifier The identifier as defined in metadata + * @return The question items with the corresponding identifier + */ + public List<QuestionItem> loadItemByIdentifier(String identifier); + public List<QuestionItemFull> getAllItems(int firstResult, int maxResults); public QuestionItem updateItem(QuestionItem item); diff --git a/src/main/java/org/olat/modules/qpool/QuestionItem.java b/src/main/java/org/olat/modules/qpool/QuestionItem.java index a94962e175d0d32dbc51ef914407b40b8c50204b..122799ee7ce8d7ba2aa3a7884a83df5996774310 100644 --- a/src/main/java/org/olat/modules/qpool/QuestionItem.java +++ b/src/main/java/org/olat/modules/qpool/QuestionItem.java @@ -22,6 +22,7 @@ package org.olat.modules.qpool; 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.taxonomy.TaxonomyLevel; /** * @@ -46,6 +47,8 @@ public interface QuestionItem extends QuestionItemShort { */ public String getTaxonomicPath(); + public TaxonomyLevel getTaxonomyLevel(); + //educational public QEducationalContext getEducationalContext(); diff --git a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java index c3ad7e6df11fe8169f5422bc1eec02f121fca02c..12ed76b7cb5cdc702119b2d49bca6ce390e4d130 100644 --- a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java +++ b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java @@ -291,6 +291,28 @@ public class QuestionItemDAO { return items.get(0); } + /** + * The method loads the question items and fetch + * the taxonomy level, license, item type and + * educational context. + * + * @param key The identifier of the item as defined in its metadata + * @return The question items with the corresponding identifier + */ + public List<QuestionItem> loadByIdentifier(String identifier) { + StringBuilder sb = new StringBuilder(); + sb.append("select item from questionitem item") + .append(" left join fetch item.taxonomyLevel taxonomyLevel") + .append(" left join fetch item.license license") + .append(" left join fetch item.type itemType") + .append(" left join fetch item.educationalContext educationalContext") + .append(" where item.identifier=:identifier"); + return dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), QuestionItem.class) + .setParameter("identifier", identifier) + .getResultList(); + } + public List<QuestionItemFull> loadByIds(Collection<Long> key) { StringBuilder sb = new StringBuilder(); sb.append("select item from questionitem item") diff --git a/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java b/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java index 1a77bc9065fe0345def3113a9f78e58e4f7ecf9e..3465757eb80558b7fbdfb7ca34e4ad956b792c84 100644 --- a/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java +++ b/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java @@ -195,6 +195,11 @@ public class QuestionPoolServiceImpl implements QPoolService { public QuestionItem loadItemById(Long key) { return questionItemDao.loadById(key); } + + @Override + public List<QuestionItem> loadItemByIdentifier(String identifier) { + return questionItemDao.loadByIdentifier(identifier); + } @Override public QuestionItem updateItem(QuestionItem item) { diff --git a/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java b/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java index fcbcd7429a5dc4e66cdf063277251d41897c4aa4..6017901a875966b14e791d6280cf49c04b29cb1a 100644 --- a/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java +++ b/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java @@ -254,7 +254,16 @@ public class QuestionListController extends AbstractItemListController implement Long itemKey = entry.getOLATResourceable().getResourceableId(); ItemRow row = getModel().getObjectByKey(itemKey); if(row == null) { - //TODO xhr + getModel().load(null, null, null, 0, -1); + row = getModel().getObjectByKey(itemKey); + if(row != null) { + doOpenDetails(ureq, row); + int index = getModel().getObjects().indexOf(row); + if(index >= 1 && getItemsTable().getPageSize() > 1) { + int page = index / getItemsTable().getPageSize(); + getItemsTable().setPage(page); + } + } } else { doOpenDetails(ureq, row); } diff --git a/src/main/java/org/olat/modules/qpool/ui/QuestionPoolMainEditorController.java b/src/main/java/org/olat/modules/qpool/ui/QuestionPoolMainEditorController.java index e0f0c40823be4ce87e34990a40133f407628635d..b6007d0ef3110f4e21e0d70903911fc62e0b2f5e 100644 --- a/src/main/java/org/olat/modules/qpool/ui/QuestionPoolMainEditorController.java +++ b/src/main/java/org/olat/modules/qpool/ui/QuestionPoolMainEditorController.java @@ -43,6 +43,7 @@ import org.olat.core.gui.control.generic.closablewrapper.CloseableModalControlle import org.olat.core.gui.control.generic.dtabs.Activateable2; import org.olat.core.gui.control.generic.modal.DialogBoxController; import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory; +import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.id.Persistable; import org.olat.core.id.context.ContextEntry; @@ -54,8 +55,11 @@ import org.olat.group.BusinessGroup; import org.olat.modules.qpool.Pool; import org.olat.modules.qpool.QPoolService; import org.olat.modules.qpool.QuestionItem; +import org.olat.modules.qpool.QuestionItem2Pool; +import org.olat.modules.qpool.QuestionItem2Resource; import org.olat.modules.qpool.QuestionItemCollection; import org.olat.modules.qpool.QuestionItemShort; +import org.olat.modules.qpool.QuestionStatus; import org.olat.modules.qpool.ui.events.QItemMarkedEvent; import org.olat.modules.qpool.ui.events.QPoolEvent; import org.olat.modules.qpool.ui.tree.CollectionTreeNode; @@ -220,20 +224,81 @@ public class QuestionPoolMainEditorController extends BasicController implements @Override public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { if(entries == null || entries.isEmpty()) return; + + OLATResourceable resource = entries.get(0).getOLATResourceable(); + if("QuestionItem".equalsIgnoreCase(resource.getResourceableTypeName())) { + activateQuestionItem(ureq, resource.getResourceableId(), entries); + } else { + TreeNode rootNode = menuTree.getTreeModel().getRootNode(); + TreeNode node = TreeHelper.findNodeByResourceableUserObject(resource, rootNode); + if(node == null) { + node = TreeHelper.findNodeByUserObject(resource.getResourceableTypeName(), rootNode); + } + if(node != null) { + List<ContextEntry> subEntries = entries.subList(1, entries.size()); + stackPanel.popUpToRootController(ureq); + doSelectControllerTreeNode(ureq, node, subEntries, entries.get(0).getTransientState()); + menuTree.setSelectedNode(node); + } + } + } + + private void activateQuestionItem(UserRequest ureq, Long itemKey, List<ContextEntry> entries) { + QuestionItem item = qpoolService.loadItemById(itemKey); + if(item == null) return; + + List<Identity> authors = qpoolService.getAuthors(item); + if(authors.contains(getIdentity())) { + activateNode(ureq, treeModel.getMyQuestionsNode(), entries); + return; + } - ContextEntry entry = entries.get(0); - OLATResourceable resource = entry.getOLATResourceable(); - TreeNode rootNode = menuTree.getTreeModel().getRootNode(); - TreeNode node = TreeHelper.findNodeByResourceableUserObject(resource, rootNode); - if(node == null) { - node = TreeHelper.findNodeByUserObject(resource.getResourceableTypeName(), rootNode); + if(item.getTaxonomyLevel() != null && item.getQuestionStatus() != null + && QuestionStatus.finalVersion.equals(item.getQuestionStatus()) && treeModel.getFinalNode() != null) { + TreeNode levelNode = treeModel.getFinalTanonomyLevelNode(item.getTaxonomyLevel()); + if(levelNode != null) { + activateNode(ureq, levelNode, entries); + return; + } } - if(node != null) { - List<ContextEntry> subEntries = entries.subList(1, entries.size()); - stackPanel.popUpToRootController(ureq); - doSelectControllerTreeNode(ureq, node, subEntries, entry.getTransientState()); - menuTree.setSelectedNode(node); + + if(treeModel.getSharesNode() != null) { + List<QuestionItem2Resource> shares = qpoolService.getSharedResourceInfosByItem(item); + for(QuestionItem2Resource share:shares) { + TreeNode levelNode = treeModel.getShareNode(share); + if(levelNode != null) { + activateNode(ureq, levelNode, entries); + return; + } + } + + List<QuestionItem2Pool> pools = qpoolService.getPoolInfosByItem(item); + for(QuestionItem2Pool pool:pools) { + TreeNode levelNode = treeModel.getShareNode(pool); + if(levelNode != null) { + activateNode(ureq, levelNode, entries); + return; + } + } + } + } + + /** + * + * @param ureq The user request + * @param nodeToActivate The tree node to activate + * @param entries The entries which map to the question + */ + private void activateNode(UserRequest ureq, TreeNode nodeToActivate, List<ContextEntry> entries) { + if(nodeToActivate == null || entries == null || entries.size() < 1) return; + + stackPanel.popUpToRootController(ureq); + if(entries == null || entries.isEmpty()) { + doSelectControllerTreeNode(ureq, nodeToActivate, null, null); + } else { + doSelectControllerTreeNode(ureq, nodeToActivate, entries, entries.get(0).getTransientState()); } + menuTree.setSelectedNode(nodeToActivate); } private void doDrop(UserRequest ureq, String targetId, String dropId) { diff --git a/src/main/java/org/olat/modules/qpool/ui/tree/BusinessGroupTreeNode.java b/src/main/java/org/olat/modules/qpool/ui/tree/BusinessGroupTreeNode.java index 3f76e5d6ae232a691ec1f1ba4fb8b377ed747d43..e8f84d015b402ae104fce16e71b11cd7867f08fa 100644 --- a/src/main/java/org/olat/modules/qpool/ui/tree/BusinessGroupTreeNode.java +++ b/src/main/java/org/olat/modules/qpool/ui/tree/BusinessGroupTreeNode.java @@ -68,6 +68,10 @@ public class BusinessGroupTreeNode extends GenericTreeNode implements Controller this.setUserObject(group); } + public BusinessGroup getBusinessGroup() { + return group; + } + @Override public Controller getController(UserRequest ureq, WindowControl wControl) { if(questionsCtrl == null) { diff --git a/src/main/java/org/olat/modules/qpool/ui/tree/FinalTreeNode.java b/src/main/java/org/olat/modules/qpool/ui/tree/FinalTreeNode.java index 39bff669ff8e575ffe82a78761a13843686e14ea..0f1adcb4cd2e504071797699cc4fc0d35efadd01 100644 --- a/src/main/java/org/olat/modules/qpool/ui/tree/FinalTreeNode.java +++ b/src/main/java/org/olat/modules/qpool/ui/tree/FinalTreeNode.java @@ -68,6 +68,10 @@ public class FinalTreeNode extends GenericTreeNode implements ControllerTreeNode this.setUserObject(taxonomyLevel); } + + public TaxonomyLevel getTanonomyLevel() { + return taxonomyLevel; + } @Override public Controller getController(UserRequest ureq, WindowControl wControl) { diff --git a/src/main/java/org/olat/modules/qpool/ui/tree/PoolTreeNode.java b/src/main/java/org/olat/modules/qpool/ui/tree/PoolTreeNode.java index 191316bcb80a2cc01cfc8c417c69bb21d41e1c81..4d4599357fef4b84a1197817357aab1b45a9efa2 100644 --- a/src/main/java/org/olat/modules/qpool/ui/tree/PoolTreeNode.java +++ b/src/main/java/org/olat/modules/qpool/ui/tree/PoolTreeNode.java @@ -67,6 +67,10 @@ public class PoolTreeNode extends GenericTreeNode implements ControllerTreeNode // The user object is used to findNodeByPersistableUserObject this.setUserObject(pool); } + + public Pool getPool() { + return pool; + } @Override public Controller getController(UserRequest ureq, WindowControl wControl) { diff --git a/src/main/java/org/olat/modules/qpool/ui/tree/QuestionPoolMenuTreeModel.java b/src/main/java/org/olat/modules/qpool/ui/tree/QuestionPoolMenuTreeModel.java index 8becc206d5b657206b2632bdb5596dae57e3dab0..beab2655ca09da7f910e9e56fa562d18d2275131 100644 --- a/src/main/java/org/olat/modules/qpool/ui/tree/QuestionPoolMenuTreeModel.java +++ b/src/main/java/org/olat/modules/qpool/ui/tree/QuestionPoolMenuTreeModel.java @@ -39,6 +39,8 @@ import org.olat.group.BusinessGroup; import org.olat.modules.qpool.Pool; import org.olat.modules.qpool.QPoolSecurityCallback; import org.olat.modules.qpool.QPoolService; +import org.olat.modules.qpool.QuestionItem2Pool; +import org.olat.modules.qpool.QuestionItem2Resource; import org.olat.modules.qpool.QuestionItemCollection; import org.olat.modules.qpool.security.QPoolSecurityCallbackFactory; import org.olat.modules.qpool.ui.QuestionPoolMainEditorController; @@ -104,6 +106,58 @@ public class QuestionPoolMenuTreeModel extends GenericTreeModel implements DnDTr return sharesNode; } + /** + * @return The node which holds the taxonomy levels for the questions in final state + * or null if the the review process is disabled. + */ + public TreeNode getFinalNode() { + return finalNode; + } + + public TreeNode getFinalTanonomyLevelNode(TaxonomyLevel level) { + if(level == null || finalNode == null) return null; + + for(int i=finalNode.getChildCount(); i-->0; ) { + INode node = finalNode.getChildAt(i); + if(node instanceof FinalTreeNode && level.equals(((FinalTreeNode)node).getTanonomyLevel())) { + return (TreeNode)node; + } + } + return null; + } + + public TreeNode getShareNode(QuestionItem2Resource share) { + if(sharesNode == null || share == null) return null; + + Long key = share.getResourceKey(); + for(int i=sharesNode.getChildCount(); i-->0; ) { + INode node = sharesNode.getChildAt(i); + if(node instanceof BusinessGroupTreeNode) { + BusinessGroup group = ((BusinessGroupTreeNode)node).getBusinessGroup(); + if(group.getResource().getKey().equals(key)) { + return (TreeNode)node; + } + } + } + return null; + } + + public TreeNode getShareNode(QuestionItem2Pool share) { + if(sharesNode == null || share == null) return null; + + Long key = share.getPoolKey(); + for(int i=sharesNode.getChildCount(); i-->0; ) { + INode node = sharesNode.getChildAt(i); + if(node instanceof PoolTreeNode) { + Pool pool = ((PoolTreeNode)node).getPool(); + if(pool.getKey().equals(key)) { + return (TreeNode)node; + } + } + } + return null; + } + public Collection<String> getDefaultOpenNodeIds() { Collection<String> openNodeIds = new ArrayList<>(4); if (myNode != null) { diff --git a/src/test/java/org/olat/ims/qti21/model/xml/ManifestPackageTest.java b/src/test/java/org/olat/ims/qti21/model/xml/ManifestPackageTest.java index 1dad45bbb670d037a9e0a98863f2f39cbb72a852..1066e577a2623abb6f3483be6abd5510c8a871d3 100644 --- a/src/test/java/org/olat/ims/qti21/model/xml/ManifestPackageTest.java +++ b/src/test/java/org/olat/ims/qti21/model/xml/ManifestPackageTest.java @@ -22,6 +22,8 @@ package org.olat.ims.qti21.model.xml; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.util.List; import java.util.UUID; @@ -29,6 +31,8 @@ import org.junit.Assert; import org.junit.Test; import org.olat.core.util.FileUtils; import org.olat.core.util.WebappHelper; +import org.olat.imsqti.xml.manifest.QTIMetadataType; +import org.olat.oo.xml.manifest.OpenOLATMetadataType; import uk.ac.ed.ph.jqtiplus.utils.contentpackaging.ContentPackageResource; import uk.ac.ed.ph.jqtiplus.utils.contentpackaging.ImsManifestException; @@ -74,4 +78,65 @@ public class ManifestPackageTest { Assert.assertNotNull(reloadManifest); FileUtils.deleteDirsAndFiles(tmpDir.toPath()); } + + /** + * + * @throws URISyntaxException + */ + @Test + public void readManifest() throws URISyntaxException { + URL xmlUrl = ManifestPackageTest.class.getResource("resources/manifest/oo_assessmentitem_imsmanifest_12_2.xml"); + File xmlFile = new File(xmlUrl.toURI()); + ManifestBuilder manifest = ManifestBuilder.read(xmlFile); + + + ManifestMetadataBuilder questionMetadata = manifest.getResourceBuilderByHref("sca9b540c9684ba58f489f02e8b5c590.xml"); + Assert.assertNotNull(questionMetadata); + + //LOM + String title = questionMetadata.getTitle(); + Assert.assertEquals("Metadata", title); + String identifier = questionMetadata.getIdentifier(); + Assert.assertEquals("id9f1ae47b-dc7f-482e-a688-111287f99fa6", identifier); + + + String keywords = questionMetadata.getGeneralKeywords(); + Assert.assertTrue(keywords.contains("Meta")); + Assert.assertTrue(keywords.contains("data")); + Assert.assertTrue(keywords.contains("keywords")); + + String context = "de"; + Assert.assertEquals("de", context); + + //educational + String educationContext = questionMetadata.getEducationContext(); + Assert.assertEquals("Primarschule", educationContext); + String typicalLearningTime = "P1DT2H3M4S"; + Assert.assertEquals("P1DT2H3M4S", typicalLearningTime); + + //lifecycle + String version = "1.0"; + Assert.assertEquals("1.0", version); + + // classification + String taxonomyPath = questionMetadata.getClassificationTaxonomy(); + Assert.assertEquals("/Mathematik/Topologie", taxonomyPath); + + //QTI 2.1 + QTIMetadataType qtiMetadata = questionMetadata.getQtiMetadata(false); + Assert.assertTrue(qtiMetadata.getInteractionType().contains("choiceInteraction")); + Assert.assertEquals("OpenOLAT", qtiMetadata.getToolName()); + Assert.assertEquals("12.3a", qtiMetadata.getToolVersion()); + + //OpenOLAT specific + OpenOLATMetadataType openolatMetadata = questionMetadata.getOpenOLATMetadata(false); + Assert.assertEquals(Double.valueOf(0.5d), openolatMetadata.getDiscriminationIndex()); + Assert.assertEquals(Double.valueOf(0.3d), openolatMetadata.getDifficulty()); + Assert.assertEquals(Double.valueOf(0.4d), openolatMetadata.getStandardDeviation()); + Assert.assertEquals(Integer.valueOf(1), openolatMetadata.getDistractors()); + Assert.assertEquals("sc", openolatMetadata.getQuestionType()); + Assert.assertEquals(Integer.valueOf(12), openolatMetadata.getUsage()); + Assert.assertEquals("formative", openolatMetadata.getAssessmentType()); + + } } diff --git a/src/test/java/org/olat/ims/qti21/model/xml/resources/manifest/oo_assessmentitem_imsmanifest_12_2.xml b/src/test/java/org/olat/ims/qti21/model/xml/resources/manifest/oo_assessmentitem_imsmanifest_12_2.xml new file mode 100644 index 0000000000000000000000000000000000000000..5030188e4709a456b42bf3d41b0a16a77d797f1b --- /dev/null +++ b/src/test/java/org/olat/ims/qti21/model/xml/resources/manifest/oo_assessmentitem_imsmanifest_12_2.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<manifest xmlns="http://www.imsglobal.org/xsd/imscp_v1p1" + xmlns:imsmd="http://www.imsglobal.org/xsd/imsmd_v1p2" xmlns:imsqti="http://www.imsglobal.org/xsd/imsqti_metadata_v2p1" + xmlns:ns4="http://www.openolat.org/xsd/oomd_v1p1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.imsglobal.org/xsd/imscp_v1p1 http://www.imsglobal.org/xsd/imscp_v1p2.xsd http://www.imsglobal.org/xsd/imsmd_v1p2 http://www.imsglobal.org/xsd/imsmd_v1p2p4.xsd http://www.imsglobal.org/xsd/imsqti_metadata_v2p1 http://www.imsglobal.org/xsd/qti/qtiv2p1/imsqti_metadata_v2p1.xsd"> + <metadata> + <schema>QTIv2.1 Package</schema> + <schemaversion>1.0.0</schemaversion> + <imsmd:lom /> + </metadata> + <resources> + <resource identifier="item2e6d877f4ba39838122dc88baba1" type="imsqti_item_xmlv2p1" + href="sca9b540c9684ba58f489f02e8b5c590.xml"> + <metadata> + <imsmd:lom> + <imsmd:general> + <imsmd:identifier>id9f1ae47b-dc7f-482e-a688-111287f99fa6</imsmd:identifier> + <imsmd:title> + <imsmd:langstring xml:lang="de">Metadata</imsmd:langstring> + </imsmd:title> + <imsmd:keyword> + <imsmd:langstring xml:lang="de">Meta</imsmd:langstring> + </imsmd:keyword> + <imsmd:keyword> + <imsmd:langstring xml:lang="de">data</imsmd:langstring> + </imsmd:keyword> + <imsmd:keyword> + <imsmd:langstring xml:lang="de">keywords</imsmd:langstring> + </imsmd:keyword> + <imsmd:coverage> + <imsmd:langstring xml:lang="de">Coverage</imsmd:langstring> + </imsmd:coverage> + <imsmd:context xsi:type="imsmd:stringType" xml:lang="de">de</imsmd:context> + </imsmd:general> + <imsmd:lifecycle> + <imsmd:version> + <imsmd:langstring xml:lang="en">1.0</imsmd:langstring> + </imsmd:version> + <imsmd:status> + <imsmd:source> + <imsmd:langstring xml:lang="x-none">LOMv1.0</imsmd:langstring> + </imsmd:source> + <imsmd:value> + <imsmd:langstring xml:lang="x-none">Draft</imsmd:langstring> + </imsmd:value> + </imsmd:status> + </imsmd:lifecycle> + <imsmd:educational> + <imsmd:context> + <imsmd:source> + <imsmd:langstring xml:lang="de">https://www.openolat.org</imsmd:langstring> + </imsmd:source> + <imsmd:value> + <imsmd:langstring xml:lang="de">Primarschule</imsmd:langstring> + </imsmd:value> + </imsmd:context> + <imsmd:typicallearningtime> + <imsmd:datetime>P1DT2H3M4S</imsmd:datetime> + </imsmd:typicallearningtime> + </imsmd:educational> + <imsmd:rights> + <imsmd:copyrightandotherrestrictions> + <imsmd:source> + <imsmd:langstring xml:lang="en">https://www.openolat.org</imsmd:langstring> + </imsmd:source> + <imsmd:value> + <imsmd:langstring xml:lang="en">Licence</imsmd:langstring> + </imsmd:value> + </imsmd:copyrightandotherrestrictions> + </imsmd:rights> + <imsmd:classification> + <imsmd:purpose> + <imsmd:source> + <imsmd:langstring xml:lang="de">LOMv1.0</imsmd:langstring> + </imsmd:source> + <imsmd:value> + <imsmd:langstring xml:lang="de">discipline</imsmd:langstring> + </imsmd:value> + </imsmd:purpose> + <imsmd:taxonpath> + <imsmd:source> + <imsmd:langstring xml:lang="en">Unkown</imsmd:langstring> + </imsmd:source> + <imsmd:taxon> + <imsmd:entry> + <imsmd:langstring xml:lang="de">Mathematik</imsmd:langstring> + </imsmd:entry> + </imsmd:taxon> + <imsmd:taxon> + <imsmd:entry> + <imsmd:langstring xml:lang="de">Topologie</imsmd:langstring> + </imsmd:entry> + </imsmd:taxon> + </imsmd:taxonpath> + </imsmd:classification> + </imsmd:lom> + <imsqti:qtiMetadata> + <imsqti:interactionType>choiceInteraction</imsqti:interactionType> + <imsqti:toolName>OpenOLAT</imsqti:toolName> + <imsqti:toolVersion>12.3a</imsqti:toolVersion> + </imsqti:qtiMetadata> + <ns4:ooMetadata> + <ns4:questionType>sc</ns4:questionType> + <ns4:difficulty>0.3</ns4:difficulty> + <ns4:standardDeviation>0.4</ns4:standardDeviation> + <ns4:discriminationIndex>0.5</ns4:discriminationIndex> + <ns4:distractors>1</ns4:distractors> + <ns4:assessmentType>formative</ns4:assessmentType> + <ns4:usage>12</ns4:usage> + </ns4:ooMetadata> + </metadata> + <file href="sca9b540c9684ba58f489f02e8b5c590.xml" /> + </resource> + </resources> +</manifest>