From 4a460153c68fb78bc6b3dd8a46c9cbe5cb2e45df Mon Sep 17 00:00:00 2001 From: srosse <none@none> Date: Wed, 27 Feb 2013 10:50:37 +0100 Subject: [PATCH] OO-535: implements import of QTI 1.2 items --- pom.xml | 2 +- src/main/java/org/olat/core/util/ZipUtil.java | 29 +++ .../olat/ims/qti/QTI12PreviewController.java | 67 +++++-- .../qti/QTIQuestionPoolServiceProvider.java | 22 +- .../ItemFileResourceValidator.java | 189 ++++++++++++++++++ .../qti/repository/handlers/QTIHandler.java | 2 +- .../olat/ims/resources/dtd/ims_qtiasiv1p2.dtd | 1 + .../olat/ims/resources/dtd/ims_qtiresv1p2.dtd | 1 + .../org/olat/modules/qpool/QuestionItem.java | 1 + .../olat/modules/qpool/QuestionItemShort.java | 2 + .../modules/qpool/QuestionPoolModule.java | 43 +++- .../olat/modules/qpool/QuestionPoolSPI.java | 9 + .../qpool/QuestionPoolSPIComparator.java | 38 ++++ .../modules/qpool/QuestionPoolService.java | 9 + .../impl/PdfQuestionPoolServiceProvider.java | 76 +++++++ .../impl/TextQuestionPoolServiceProvider.java | 70 +++++++ .../qpool/manager/NullPoolService.java | 2 +- .../qpool/manager/QuestionItemDAO.java | 41 +++- .../manager/QuestionPoolServiceImpl.java | 128 ++++++++++++ .../modules/qpool/manager/StudyFieldDAO.java | 4 + .../modules/qpool/model/QuestionItemImpl.java | 35 +++- .../ui/ImportQuestionItemController.java | 84 ++++++++ .../modules/qpool/ui/QuestionItemRow.java | 5 + .../qpool/ui/QuestionListController.java | 25 ++- .../modules/qpool/ui/_content/item_list.html | 1 + .../qpool/ui/_i18n/LocalStrings_de.properties | 3 +- .../database/mysql/alter_8_4_0_to_9_0_0.sql | 7 +- .../FileResourceValidatorTest.java | 46 +++++ .../ims/qti/fileresource/mchc_i_002.zip | Bin 0 -> 13846 bytes .../ims/qti/fileresource/mchc_ir_005.xml | 1 + .../qpool/manager/CollectionDAOTest.java | 6 +- .../modules/qpool/manager/PoolDAOTest.java | 6 +- .../qpool/manager/QuestionDAOTest.java | 27 +-- .../manager/QuestionPoolServiceTest.java | 31 ++- .../olat/modules/qpool/manager/mchc_i_001.xml | 1 + .../java/org/olat/test/AllTestsJunit4.java | 1 + 36 files changed, 952 insertions(+), 63 deletions(-) create mode 100644 src/main/java/org/olat/ims/qti/fileresource/ItemFileResourceValidator.java create mode 100644 src/main/java/org/olat/ims/resources/dtd/ims_qtiasiv1p2.dtd create mode 100644 src/main/java/org/olat/ims/resources/dtd/ims_qtiresv1p2.dtd create mode 100644 src/main/java/org/olat/modules/qpool/QuestionPoolSPIComparator.java create mode 100644 src/main/java/org/olat/modules/qpool/impl/PdfQuestionPoolServiceProvider.java create mode 100644 src/main/java/org/olat/modules/qpool/impl/TextQuestionPoolServiceProvider.java create mode 100644 src/main/java/org/olat/modules/qpool/ui/ImportQuestionItemController.java create mode 100644 src/test/java/org/olat/modules/ims/qti/fileresource/FileResourceValidatorTest.java create mode 100644 src/test/java/org/olat/modules/ims/qti/fileresource/mchc_i_002.zip create mode 100644 src/test/java/org/olat/modules/ims/qti/fileresource/mchc_ir_005.xml create mode 100644 src/test/java/org/olat/modules/qpool/manager/mchc_i_001.xml diff --git a/pom.xml b/pom.xml index 272c5dc91be..1a20a051b55 100644 --- a/pom.xml +++ b/pom.xml @@ -1528,7 +1528,7 @@ <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> - <version>1.4</version> + <version>2.4</version> </dependency> <dependency> <groupId>org.mnode.ical4j</groupId> diff --git a/src/main/java/org/olat/core/util/ZipUtil.java b/src/main/java/org/olat/core/util/ZipUtil.java index 36f4e3c851f..c64e6a4ad33 100644 --- a/src/main/java/org/olat/core/util/ZipUtil.java +++ b/src/main/java/org/olat/core/util/ZipUtil.java @@ -30,6 +30,7 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -45,6 +46,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; +import org.apache.commons.io.IOUtils; import org.olat.core.commons.modules.bc.meta.MetaInfo; import org.olat.core.commons.modules.bc.meta.MetaInfoHelper; import org.olat.core.commons.modules.bc.meta.tagged.MetaTagged; @@ -125,6 +127,33 @@ public class ZipUtil { return unzip(zipLeaf, targetDir, null, false); } + /** + * Unzip a file in the target dir with the restricted version + * @param zipFile + * @param targetDir + * @return + */ + public static boolean unzipStrict(File zipFile, VFSContainer targetDir) { + if (targetDir instanceof LocalFolderImpl) { + String outdir = ((LocalFolderImpl) targetDir).getBasefile().getAbsolutePath(); + InputStream in = null; + try { + long s = System.currentTimeMillis(); + in = new FileInputStream(zipFile); + xxunzip (in, outdir); + log.info("unzip file="+zipFile.getName()+" to="+outdir +" t="+Long.toString(System.currentTimeMillis()-s)); + return true; + } catch (IOException e) { + log.error("I/O failure while unzipping "+zipFile.getName()+" to "+outdir); + return false; + } finally { + IOUtils.closeQuietly(in); + } + } + return false; + } + + /** * Unzip a file to a directory using the versioning system of VFS * @param zipLeaf The file to unzip diff --git a/src/main/java/org/olat/ims/qti/QTI12PreviewController.java b/src/main/java/org/olat/ims/qti/QTI12PreviewController.java index 2196fcdfec5..89b87e97ae7 100644 --- a/src/main/java/org/olat/ims/qti/QTI12PreviewController.java +++ b/src/main/java/org/olat/ims/qti/QTI12PreviewController.java @@ -19,27 +19,36 @@ */ package org.olat.ims.qti; -import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; +import javax.servlet.http.HttpServletRequest; + import org.dom4j.Document; import org.dom4j.Element; +import org.olat.core.CoreSpringFactory; +import org.olat.core.dispatcher.mapper.Mapper; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.panel.Panel; 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.gui.media.MediaResource; +import org.olat.core.gui.media.NotFoundMediaResource; import org.olat.core.gui.translator.Translator; import org.olat.core.util.Util; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.VFSMediaResource; import org.olat.core.util.xml.XMLParser; import org.olat.ims.qti.editor.ItemPreviewController; import org.olat.ims.qti.editor.QTIEditorPackage; import org.olat.ims.qti.editor.beecom.objects.Item; import org.olat.ims.qti.editor.beecom.parser.ParserManager; import org.olat.ims.resources.IMSEntityResolver; - +import org.olat.modules.qpool.QuestionItem; +import org.olat.modules.qpool.QuestionPoolService; /** * * Initial date: 21.02.2013<br> @@ -50,30 +59,36 @@ public class QTI12PreviewController extends BasicController { private final Panel mainPanel; private ItemPreviewController previewCtrl; + private final QuestionPoolService qpoolService; - public QTI12PreviewController(UserRequest ureq, WindowControl wControl) { + public QTI12PreviewController(UserRequest ureq, WindowControl wControl, QuestionItem qitem) { super(ureq, wControl); - + qpoolService = CoreSpringFactory.getImpl(QuestionPoolService.class); mainPanel = new Panel("qti12preview"); - Item item = readItemXml(); - if(item != null) { - Translator translator = Util.createPackageTranslator(QTIEditorPackage.class, getLocale()); - previewCtrl = new ItemPreviewController(wControl, item, "/Users/srosse", translator); - listenTo(previewCtrl); - mainPanel.setContent(previewCtrl.getInitialComponent()); + VFSLeaf leaf = qpoolService.getRootFile(qitem); + if(leaf == null) { + //no data to preview + } else { + Item item = readItemXml(leaf); + if(item != null) { + Translator translator = Util.createPackageTranslator(QTIEditorPackage.class, getLocale()); + VFSContainer directory = qpoolService.getRootDirectory(qitem); + String mapperUrl = registerMapper(ureq, new QItemDirectoryMapper(directory)); + previewCtrl = new ItemPreviewController(wControl, item, mapperUrl, translator); + listenTo(previewCtrl); + mainPanel.setContent(previewCtrl.getInitialComponent()); + } } putInitialPanel(mainPanel); } - private Item readItemXml() { - - XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); + private Item readItemXml(VFSLeaf leaf) { Document doc = null; try { - File itemXml = new File("/Users/srosse/Desktop/mchc_i_001.xml"); - InputStream is = new FileInputStream(itemXml); + InputStream is = leaf.getInputStream(); + XMLParser xmlParser = new XMLParser(new IMSEntityResolver()); doc = xmlParser.parse(is, false); Element item = (Element)doc.selectSingleNode("questestinterop/item"); @@ -81,7 +96,6 @@ public class QTI12PreviewController extends BasicController { Item qtiItem = (Item)parser.parse(item); is.close(); - return qtiItem; } catch (Exception e) { logError("", e); @@ -98,4 +112,23 @@ public class QTI12PreviewController extends BasicController { protected void event(UserRequest ureq, Component source, Event event) { // } + + private class QItemDirectoryMapper implements Mapper { + private final VFSContainer itemBaseContainer; + + private QItemDirectoryMapper(VFSContainer container) { + itemBaseContainer = container; + } + + public MediaResource handle(String relPath, HttpServletRequest request) { + VFSItem vfsItem = itemBaseContainer.resolve(relPath); + MediaResource mr; + if (vfsItem == null || !(vfsItem instanceof VFSLeaf)) { + mr = new NotFoundMediaResource(relPath); + } else { + mr = new VFSMediaResource((VFSLeaf) vfsItem); + } + return mr; + } + } } diff --git a/src/main/java/org/olat/ims/qti/QTIQuestionPoolServiceProvider.java b/src/main/java/org/olat/ims/qti/QTIQuestionPoolServiceProvider.java index f91d11c5b4a..807ea9e54b8 100644 --- a/src/main/java/org/olat/ims/qti/QTIQuestionPoolServiceProvider.java +++ b/src/main/java/org/olat/ims/qti/QTIQuestionPoolServiceProvider.java @@ -19,9 +19,13 @@ */ package org.olat.ims.qti; +import java.io.File; + import org.olat.core.gui.UserRequest; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.ims.qti.fileresource.ItemFileResourceValidator; import org.olat.modules.qpool.QuestionItem; import org.olat.modules.qpool.QuestionPoolSPI; @@ -33,20 +37,34 @@ import org.olat.modules.qpool.QuestionPoolSPI; */ public class QTIQuestionPoolServiceProvider implements QuestionPoolSPI { + @Override + public int getPriority() { + return 10; + } + @Override public String getFormat() { return QTIConstants.QTI_12_FORMAT; } + @Override + public boolean isCompatible(String filename, File file) { + return new ItemFileResourceValidator().validate(filename, file); + } + @Override + public boolean isCompatible(String filename, VFSLeaf file) { + return new ItemFileResourceValidator().validate(filename, file); + } + @Override public Controller getPreviewController(UserRequest ureq, WindowControl wControl, QuestionItem item) { - QTI12PreviewController previewCtrl = new QTI12PreviewController(ureq, wControl); + QTI12PreviewController previewCtrl = new QTI12PreviewController(ureq, wControl, item); return previewCtrl; } @Override public Controller getEditableController(UserRequest ureq, WindowControl wControl, QuestionItem item) { - QTI12PreviewController previewCtrl = new QTI12PreviewController(ureq, wControl); + QTI12PreviewController previewCtrl = new QTI12PreviewController(ureq, wControl, item); return previewCtrl; } } \ No newline at end of file diff --git a/src/main/java/org/olat/ims/qti/fileresource/ItemFileResourceValidator.java b/src/main/java/org/olat/ims/qti/fileresource/ItemFileResourceValidator.java new file mode 100644 index 00000000000..afd27c2a787 --- /dev/null +++ b/src/main/java/org/olat/ims/qti/fileresource/ItemFileResourceValidator.java @@ -0,0 +1,189 @@ +/** + * <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.qti.fileresource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.commons.io.IOUtils; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.ims.resources.IMSEntityResolver; +import org.olat.search.service.document.file.utils.ShieldInputStream; +import org.xml.sax.Attributes; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +/** + * + * Initial date: 27.02.2013<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ItemFileResourceValidator { + + private static final OLog log = Tracing.createLoggerFor(ItemFileResourceValidator.class); + + public boolean validate(String filename, File file) { + InputStream in = null; + try { + in = new FileInputStream(file); + return validate(filename, in); + } catch (FileNotFoundException e) { + return false; + } finally { + IOUtils.closeQuietly(in); + } + } + + public boolean validate(String filename, VFSLeaf file) { + InputStream in = null; + try { + in = file.getInputStream(); + return validate(filename, in); + } catch (Exception e) { + return false; + } finally { + IOUtils.closeQuietly(in); + } + } + + public boolean validate(String filename, InputStream in) { + boolean valid = false; + + if(filename.toLowerCase().endsWith(".xml")) { + valid = validateXml(in); + IOUtils.closeQuietly(in); + } else if(filename.toLowerCase().endsWith(".zip")) { + ZipInputStream oZip = new ZipInputStream(in); + try { + ZipEntry oEntr = oZip.getNextEntry(); + while (oEntr != null) { + if (!oEntr.isDirectory()) { + if(validateXml(new ShieldInputStream(oZip))) { + valid = true; + } + } + oZip.closeEntry(); + oEntr = oZip.getNextEntry(); + } + } catch(Exception e) { + log.error("", e); + valid = false; + } finally { + IOUtils.closeQuietly(oZip); + IOUtils.closeQuietly(in); + } + } + + return valid; + } + + private boolean validateXml(InputStream in) { + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setValidating(true); + factory.setNamespaceAware(true); + + SimpleErrorHandler errorHandler = new SimpleErrorHandler(); + ItemContentHandler contentHandler = new ItemContentHandler(); + + SAXParser parser = factory.newSAXParser(); + XMLReader reader = parser.getXMLReader(); + reader.setEntityResolver(new IMSEntityResolver()); + reader.setErrorHandler(errorHandler); + reader.setContentHandler(contentHandler); + reader.parse(new InputSource(in)); + return errorHandler.isValid() && contentHandler.isItem(); + } catch (ParserConfigurationException e) { + return false; + } catch (SAXException e) { + return false; + } catch (Exception e) { + return false; + } + } + + private static class SimpleErrorHandler implements ErrorHandler { + private int error = 0; + + public boolean isValid() { + return error == 0; + } + + @Override + public void warning(SAXParseException exception) throws SAXException { + // + } + + @Override + public void error(SAXParseException exception) throws SAXException { + error++; + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + error++; + } + } + + private static class ItemContentHandler extends DefaultHandler { + private boolean interop; + private boolean item; + + public boolean isItem() { + return item; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + + if("questestinterop".equals(qName)) { + interop = true; + } else if("item".equals(localName)) { + if(interop) { + item = true; + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + if("questestinterop".equals(qName)) { + interop = false; + } + } + } +} diff --git a/src/main/java/org/olat/ims/qti/repository/handlers/QTIHandler.java b/src/main/java/org/olat/ims/qti/repository/handlers/QTIHandler.java index 637036c3e85..8732dcc45d0 100644 --- a/src/main/java/org/olat/ims/qti/repository/handlers/QTIHandler.java +++ b/src/main/java/org/olat/ims/qti/repository/handlers/QTIHandler.java @@ -154,7 +154,7 @@ public abstract class QTIHandler extends FileHandler implements RepositoryHandle File fUnzippedDir = FileResourceManager.getInstance().unzipFileResource(tempFr); File changeLogDir = new File(fUnzippedDir, "changelog"); if(changeLogDir.exists()) { - boolean changeLogDeleted = FileUtils.deleteDirsAndFiles(changeLogDir, true, true); + FileUtils.deleteDirsAndFiles(changeLogDir, true, true); } File targetZipFile = sourceFile; FileUtils.deleteDirsAndFiles(targetZipFile.getParentFile(), true, false); diff --git a/src/main/java/org/olat/ims/resources/dtd/ims_qtiasiv1p2.dtd b/src/main/java/org/olat/ims/resources/dtd/ims_qtiasiv1p2.dtd new file mode 100644 index 00000000000..3ba16b4cd93 --- /dev/null +++ b/src/main/java/org/olat/ims/resources/dtd/ims_qtiasiv1p2.dtd @@ -0,0 +1 @@ +<?xml version='1.0' encoding='UTF-8' ?> <!--Generated by XML Authority--> <!-- ******************************************************* --> <!-- --> <!-- TITLE: ims_qtiasiv1p2.dtd --> <!-- TYPE: IMS Question and Test Interoperability --> <!-- Assessment, Section, Item structure and --> <!-- Objects-bank. --> <!-- --> <!-- REVISION HISTORY: --> <!-- Date Author --> <!-- ==== ====== --> <!-- 22nd Jan 2002 Colin Smythe --> <!-- --> <!-- This specification has been approved as a PDS release. --> <!-- --> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- ROOT DEFINITION --> <!-- ******************************************************* --> <!ELEMENT questestinterop (qticomment? , (objectbank | assessment | (section | item)+))> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- ENTITY DEFINITIONS --> <!-- ******************************************************* --> <!ENTITY % I_Testoperator " testoperator (EQ | NEQ | LT | LTE | GT | GTE ) #REQUIRED"> <!ENTITY % I_Pname " pname CDATA #REQUIRED"> <!ENTITY % I_Class " class CDATA 'Block'"> <!ENTITY % I_Mdoperator " mdoperator (EQ | NEQ | LT | LTE | GT | GTE ) #REQUIRED"> <!ENTITY % I_Mdname " mdname CDATA #REQUIRED"> <!ENTITY % I_Title " title CDATA #IMPLIED"> <!ENTITY % I_Label " label CDATA #IMPLIED"> <!ENTITY % I_Ident " ident CDATA #REQUIRED"> <!ENTITY % I_View " view (All | Administrator | AdminAuthority | Assessor | Author | Candidate | InvigilatorProctor | Psychometrician | Scorer | Tutor ) 'All'"> <!ENTITY % I_FeedbackSwitch " feedbackswitch (Yes | No ) 'Yes'"> <!ENTITY % I_HintSwitch " hintswitch (Yes | No ) 'Yes'"> <!ENTITY % I_SolutionSwitch " solutionswitch (Yes | No ) 'Yes'"> <!ENTITY % I_Rcardinality " rcardinality (Single | Multiple | Ordered ) 'Single'"> <!ENTITY % I_Rtiming " rtiming (Yes | No ) 'No'"> <!ENTITY % I_Uri " uri CDATA #IMPLIED"> <!ENTITY % I_X0 " x0 CDATA #IMPLIED"> <!ENTITY % I_Y0 " y0 CDATA #IMPLIED"> <!ENTITY % I_Height " height CDATA #IMPLIED"> <!ENTITY % I_Width " width CDATA #IMPLIED"> <!ENTITY % I_Embedded " embedded CDATA 'base64'"> <!ENTITY % I_LinkRefId " linkrefid CDATA #REQUIRED"> <!ENTITY % I_VarName " varname CDATA 'SCORE'"> <!ENTITY % I_RespIdent " respident CDATA #REQUIRED"> <!ENTITY % I_Continue " continue (Yes | No ) 'No'"> <!ENTITY % I_CharSet " charset CDATA 'ascii-us'"> <!ENTITY % I_ScoreModel " scoremodel CDATA #IMPLIED"> <!ENTITY % I_MinNumber " minnumber CDATA #IMPLIED"> <!ENTITY % I_MaxNumber " maxnumber CDATA #IMPLIED"> <!ENTITY % I_FeedbackStyle " feedbackstyle (Complete | Incremental | Multilevel | Proprietary ) 'Complete'"> <!ENTITY % I_Case " case (Yes | No ) 'No'"> <!ENTITY % I_EntityRef " entityref ENTITY #IMPLIED"> <!ENTITY % I_Index " index CDATA #IMPLIED"> <!ELEMENT qmd_computerscored (#PCDATA)> <!ELEMENT qmd_feedbackpermitted (#PCDATA)> <!ELEMENT qmd_hintspermitted (#PCDATA)> <!ELEMENT qmd_itemtype (#PCDATA)> <!ELEMENT qmd_maximumscore (#PCDATA)> <!ELEMENT qmd_renderingtype (#PCDATA)> <!ELEMENT qmd_responsetype (#PCDATA)> <!ELEMENT qmd_scoringpermitted (#PCDATA)> <!ELEMENT qmd_solutionspermitted (#PCDATA)> <!ELEMENT qmd_status (#PCDATA)> <!ELEMENT qmd_timedependence (#PCDATA)> <!ELEMENT qmd_timelimit (#PCDATA)> <!ELEMENT qmd_toolvendor (#PCDATA)> <!ELEMENT qmd_topic (#PCDATA)> <!ELEMENT qmd_material (#PCDATA)> <!ELEMENT qmd_typeofsolution (#PCDATA)> <!ELEMENT qmd_levelofdifficulty (#PCDATA)> <!ELEMENT qmd_weighting (#PCDATA)> <!ELEMENT qtimetadata (vocabulary? , qtimetadatafield+)> <!ELEMENT vocabulary (#PCDATA)> <!ATTLIST vocabulary %I_Uri; %I_EntityRef; vocab_type CDATA #IMPLIED > <!ELEMENT qtimetadatafield (fieldlabel , fieldentry)> <!ATTLIST qtimetadatafield xml:lang CDATA #IMPLIED > <!ELEMENT fieldlabel (#PCDATA)> <!ELEMENT fieldentry (#PCDATA)> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- COMMON OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT qticomment (#PCDATA)> <!ATTLIST qticomment xml:lang CDATA #IMPLIED> <!ELEMENT material (qticomment? , (mattext | matemtext | matimage | mataudio | matvideo | matapplet | matapplication | matref | matbreak | mat_extension)+ , altmaterial*)> <!ATTLIST material %I_Label; xml:lang CDATA #IMPLIED > <!ELEMENT mattext (#PCDATA)> <!ATTLIST mattext texttype CDATA 'text/plain' %I_Label; %I_CharSet; %I_Uri; xml:space (preserve | default ) 'default' xml:lang CDATA #IMPLIED %I_EntityRef; %I_Width; %I_Height; %I_Y0; %I_X0; > <!ELEMENT matemtext (#PCDATA)> <!ATTLIST matemtext texttype CDATA 'text/plain' %I_Label; %I_CharSet; %I_Uri; xml:space (preserve | default ) 'default' xml:lang CDATA #IMPLIED %I_EntityRef; %I_Width; %I_Height; %I_Y0; %I_X0; > <!ELEMENT matimage (#PCDATA)> <!ATTLIST matimage imagtype CDATA 'image/jpeg' %I_Label; %I_Height; %I_Uri; %I_Embedded; %I_Width; %I_Y0; %I_X0; %I_EntityRef; > <!ELEMENT mataudio (#PCDATA)> <!ATTLIST mataudio audiotype CDATA 'audio/base' %I_Label; %I_Uri; %I_Embedded; %I_EntityRef; > <!ELEMENT matvideo (#PCDATA)> <!ATTLIST matvideo videotype CDATA 'video/avi' %I_Label; %I_Uri; %I_Width; %I_Height; %I_Y0; %I_X0; %I_Embedded; %I_EntityRef; > <!ELEMENT matapplet (#PCDATA)> <!ATTLIST matapplet %I_Label; %I_Uri; %I_Y0; %I_Height; %I_Width; %I_X0; %I_Embedded; %I_EntityRef; > <!ELEMENT matapplication (#PCDATA)> <!ATTLIST matapplication apptype CDATA #IMPLIED %I_Label; %I_Uri; %I_Embedded; %I_EntityRef; > <!ELEMENT matbreak EMPTY> <!ELEMENT matref EMPTY> <!ATTLIST matref %I_LinkRefId; > <!ELEMENT material_ref EMPTY> <!ATTLIST material_ref %I_LinkRefId; > <!ELEMENT altmaterial (qticomment? , (mattext | matemtext | matimage | mataudio | matvideo | matapplet | matapplication | matref | matbreak | mat_extension)+)> <!ATTLIST altmaterial xml:lang CDATA #IMPLIED > <!ELEMENT decvar (#PCDATA)> <!ATTLIST decvar %I_VarName; vartype (Integer | String | Decimal | Scientific | Boolean | Enumerated | Set ) 'Integer' defaultval CDATA #IMPLIED minvalue CDATA #IMPLIED maxvalue CDATA #IMPLIED members CDATA #IMPLIED cutvalue CDATA #IMPLIED > <!ELEMENT setvar (#PCDATA)> <!ATTLIST setvar %I_VarName; action (Set | Add | Subtract | Multiply | Divide ) 'Set' > <!ELEMENT interpretvar (material | material_ref)> <!ATTLIST interpretvar %I_View; %I_VarName; > <!ELEMENT conditionvar (not | and | or | unanswered | other | varequal | varlt | varlte | vargt | vargte | varsubset | varinside | varsubstring | durequal | durlt | durlte | durgt | durgte | var_extension)+> <!ELEMENT not (and | or | not | unanswered | other | varequal | varlt | varlte | vargt | vargte | varsubset | varinside | varsubstring | durequal | durlt | durlte | durgt | durgte)> <!ELEMENT and (not | and | or | unanswered | other | varequal | varlt | varlte | vargt | vargte | varsubset | varinside | varsubstring | durequal | durlt | durlte | durgt | durgte)+> <!ELEMENT or (not | and | or | unanswered | other | varequal | varlt | varlte | vargt | vargte | varsubset | varinside | varsubstring | durequal | durlt | durlte | durgt | durgte)+> <!ELEMENT varequal (#PCDATA)> <!ATTLIST varequal %I_Case; %I_RespIdent; %I_Index; > <!ELEMENT varlt (#PCDATA)> <!ATTLIST varlt %I_RespIdent; %I_Index; > <!ELEMENT varlte (#PCDATA)> <!ATTLIST varlte %I_RespIdent; %I_Index; > <!ELEMENT vargt (#PCDATA)> <!ATTLIST vargt %I_RespIdent; %I_Index; > <!ELEMENT vargte (#PCDATA)> <!ATTLIST vargte %I_RespIdent; %I_Index; > <!ELEMENT varsubset (#PCDATA)> <!ATTLIST varsubset %I_RespIdent; setmatch (Exact | Partial ) 'Exact' %I_Index; > <!ELEMENT varinside (#PCDATA)> <!ATTLIST varinside areatype (Ellipse | Rectangle | Bounded ) #REQUIRED %I_RespIdent; %I_Index; > <!ELEMENT varsubstring (#PCDATA)> <!ATTLIST varsubstring %I_Index; %I_RespIdent; %I_Case; > <!ELEMENT durequal (#PCDATA)> <!ATTLIST durequal %I_Index; %I_RespIdent; > <!ELEMENT durlt (#PCDATA)> <!ATTLIST durlt %I_Index; %I_RespIdent; > <!ELEMENT durlte (#PCDATA)> <!ATTLIST durlte %I_Index; %I_RespIdent; > <!ELEMENT durgt (#PCDATA)> <!ATTLIST durgt %I_Index; %I_RespIdent; > <!ELEMENT durgte (#PCDATA)> <!ATTLIST durgte %I_Index; %I_RespIdent; > <!ELEMENT unanswered (#PCDATA)> <!ATTLIST unanswered %I_RespIdent; > <!ELEMENT other (#PCDATA)> <!ELEMENT duration (#PCDATA)> <!ELEMENT displayfeedback (#PCDATA)> <!ATTLIST displayfeedback feedbacktype (Response | Solution | Hint ) 'Response' %I_LinkRefId; > <!ELEMENT objectives (qticomment? , (material+ | flow_mat+))> <!ATTLIST objectives %I_View; > <!ELEMENT rubric (qticomment? , (material+ | flow_mat+))> <!ATTLIST rubric %I_View; > <!ELEMENT flow_mat (flow_mat | material | material_ref)+> <!ATTLIST flow_mat %I_Class; > <!ELEMENT presentation_material (qticomment? , flow_mat+)> <!ELEMENT reference (qticomment | material | mattext | matemtext | matimage | mataudio | matvideo | matapplet | matapplication | matbreak | mat_extension)+> <!ELEMENT selection_ordering (qticomment? , sequence_parameter? , selection* , order?)> <!ATTLIST selection_ordering sequence_type CDATA #IMPLIED > <!ELEMENT outcomes_processing (qticomment? , outcomes , objects_condition* , processing_parameter* , map_output* , outcomes_feedback_test*)> <!ATTLIST outcomes_processing %I_ScoreModel; > <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- EXTENSION DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT mat_extension ANY> <!ELEMENT var_extension ANY> <!ELEMENT response_extension ANY> <!ELEMENT render_extension ANY> <!ELEMENT assessproc_extension ANY> <!ELEMENT sectionproc_extension ANY> <!ELEMENT itemproc_extension ANY> <!ELEMENT respcond_extension ANY> <!ELEMENT selection_extension ANY> <!ELEMENT objectscond_extension (#PCDATA)> <!ELEMENT order_extension ANY> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- OBJECT-BANK OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT objectbank (qticomment? , qtimetadata* , (section | item)+)> <!ATTLIST objectbank %I_Ident; > <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- ASSESSMENT OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT assessment (qticomment? , duration? , qtimetadata* , objectives* , assessmentcontrol* , rubric* , presentation_material? , outcomes_processing* , assessproc_extension? , assessfeedback* , selection_ordering? , reference? , (sectionref | section)+)> <!ATTLIST assessment %I_Ident; %I_Title; xml:lang CDATA #IMPLIED > <!ELEMENT assessmentcontrol (qticomment?)> <!ATTLIST assessmentcontrol %I_HintSwitch; %I_SolutionSwitch; %I_View; %I_FeedbackSwitch; > <!ELEMENT assessfeedback (qticomment? , (material+ | flow_mat+))> <!ATTLIST assessfeedback %I_View; %I_Ident; %I_Title; > <!ELEMENT sectionref (#PCDATA)> <!ATTLIST sectionref %I_LinkRefId; > <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- SECTION OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT section (qticomment? , duration? , qtimetadata* , objectives* , sectioncontrol* , sectionprecondition* , sectionpostcondition* , rubric* , presentation_material? , outcomes_processing* , sectionproc_extension? , sectionfeedback* , selection_ordering? , reference? , (itemref | item | sectionref | section)*)> <!ATTLIST section %I_Ident; %I_Title; xml:lang CDATA #IMPLIED > <!ELEMENT sectionprecondition (#PCDATA)> <!ELEMENT sectionpostcondition (#PCDATA)> <!ELEMENT sectioncontrol (qticomment?)> <!ATTLIST sectioncontrol %I_FeedbackSwitch; %I_HintSwitch; %I_SolutionSwitch; %I_View; > <!ELEMENT itemref (#PCDATA)> <!ATTLIST itemref %I_LinkRefId; > <!ELEMENT sectionfeedback (qticomment? , (material+ | flow_mat+))> <!ATTLIST sectionfeedback %I_View; %I_Ident; %I_Title; > <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- ITEM OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT item (qticomment? , duration? , itemmetadata? , objectives* , itemcontrol* , itemprecondition* , itempostcondition* , (itemrubric | rubric)* , presentation? , resprocessing* , itemproc_extension? , itemfeedback* , reference?)> <!ATTLIST item maxattempts CDATA #IMPLIED %I_Label; %I_Ident; %I_Title; xml:lang CDATA #IMPLIED > <!ELEMENT itemmetadata (qtimetadata* , qmd_computerscored? , qmd_feedbackpermitted? , qmd_hintspermitted? , qmd_itemtype? , qmd_levelofdifficulty? , qmd_maximumscore? , qmd_renderingtype* , qmd_responsetype* , qmd_scoringpermitted? , qmd_solutionspermitted? , qmd_status? , qmd_timedependence? , qmd_timelimit? , qmd_toolvendor? , qmd_topic? , qmd_weighting? , qmd_material* , qmd_typeofsolution?)> <!ELEMENT itemcontrol (qticomment?)> <!ATTLIST itemcontrol %I_FeedbackSwitch; %I_HintSwitch; %I_SolutionSwitch; %I_View; > <!ELEMENT itemprecondition (#PCDATA)> <!ELEMENT itempostcondition (#PCDATA)> <!ELEMENT itemrubric (material)> <!ATTLIST itemrubric %I_View; > <!ELEMENT presentation (qticomment? , (flow | (material | response_lid | response_xy | response_str | response_num | response_grp | response_extension)+))> <!ATTLIST presentation %I_Label; xml:lang CDATA #IMPLIED %I_Y0; %I_X0; %I_Width; %I_Height; > <!ELEMENT flow (flow | material | material_ref | response_lid | response_xy | response_str | response_num | response_grp | response_extension)+> <!ATTLIST flow %I_Class; > <!ELEMENT response_lid ((material | material_ref)? , (render_choice | render_hotspot | render_slider | render_fib | render_extension) , (material | material_ref)?)> <!ATTLIST response_lid %I_Rcardinality; %I_Rtiming; %I_Ident; > <!ELEMENT response_xy ((material | material_ref)? , (render_choice | render_hotspot | render_slider | render_fib | render_extension) , (material | material_ref)?)> <!ATTLIST response_xy %I_Rcardinality; %I_Rtiming; %I_Ident; > <!ELEMENT response_str ((material | material_ref)? , (render_choice | render_hotspot | render_slider | render_fib | render_extension) , (material | material_ref)?)> <!ATTLIST response_str %I_Rcardinality; %I_Ident; %I_Rtiming; > <!ELEMENT response_num ((material | material_ref)? , (render_choice | render_hotspot | render_slider | render_fib | render_extension) , (material | material_ref)?)> <!ATTLIST response_num numtype (Integer | Decimal | Scientific ) 'Integer' %I_Rcardinality; %I_Ident; %I_Rtiming; > <!ELEMENT response_grp ((material | material_ref)? , (render_choice | render_hotspot | render_slider | render_fib | render_extension) , (material | material_ref)?)> <!ATTLIST response_grp %I_Rcardinality; %I_Ident; %I_Rtiming; > <!ELEMENT response_label (#PCDATA | qticomment | material | material_ref | flow_mat)*> <!ATTLIST response_label rshuffle (Yes | No ) 'Yes' rarea (Ellipse | Rectangle | Bounded ) 'Ellipse' rrange (Exact | Range ) 'Exact' labelrefid CDATA #IMPLIED %I_Ident; match_group CDATA #IMPLIED match_max CDATA #IMPLIED > <!ELEMENT flow_label (flow_label | response_label)+> <!ATTLIST flow_label %I_Class; > <!ELEMENT response_na ANY> <!ELEMENT render_choice ((material | material_ref | response_label | flow_label)* , response_na?)> <!ATTLIST render_choice shuffle (Yes | No ) 'No' %I_MinNumber; %I_MaxNumber; > <!ELEMENT render_hotspot ((material | material_ref | response_label | flow_label)* , response_na?)> <!ATTLIST render_hotspot %I_MaxNumber; %I_MinNumber; showdraw (Yes | No ) 'No' > <!ELEMENT render_slider ((material | material_ref | response_label | flow_label)* , response_na?)> <!ATTLIST render_slider orientation (Horizontal | Vertical ) 'Horizontal' lowerbound CDATA #REQUIRED upperbound CDATA #REQUIRED step CDATA #IMPLIED startval CDATA #IMPLIED steplabel (Yes | No ) 'No' %I_MaxNumber; %I_MinNumber; > <!ELEMENT render_fib ((material | material_ref | response_label | flow_label)* , response_na?)> <!ATTLIST render_fib encoding CDATA 'UTF_8' fibtype (String | Integer | Decimal | Scientific ) 'String' rows CDATA #IMPLIED maxchars CDATA #IMPLIED prompt (Box | Dashline | Asterisk | Underline ) #IMPLIED columns CDATA #IMPLIED %I_CharSet; %I_MaxNumber; %I_MinNumber; > <!ELEMENT resprocessing (qticomment? , outcomes , (respcondition | itemproc_extension)+)> <!ATTLIST resprocessing %I_ScoreModel; > <!ELEMENT outcomes (qticomment? , (decvar , interpretvar*)+)> <!ELEMENT respcondition (qticomment? , conditionvar , setvar* , displayfeedback* , respcond_extension?)> <!ATTLIST respcondition %I_Continue; %I_Title; > <!ELEMENT itemfeedback ((flow_mat | material) | solution | hint)+> <!ATTLIST itemfeedback %I_View; %I_Ident; %I_Title; > <!ELEMENT solution (qticomment? , solutionmaterial+)> <!ATTLIST solution %I_FeedbackStyle; > <!ELEMENT solutionmaterial (material+ | flow_mat+)> <!ELEMENT hint (qticomment? , hintmaterial+)> <!ATTLIST hint %I_FeedbackStyle; > <!ELEMENT hintmaterial (material+ | flow_mat+)> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- SELECTION AND ORDERING OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT selection (sourcebank_ref? , selection_number? , selection_metadata? , (and_selection | or_selection | not_selection | selection_extension)?)> <!ELEMENT order (order_extension?)> <!ATTLIST order order_type CDATA #REQUIRED > <!ELEMENT selection_number (#PCDATA)> <!ELEMENT selection_metadata (#PCDATA)> <!ATTLIST selection_metadata %I_Mdname; %I_Mdoperator;> <!ELEMENT sequence_parameter (#PCDATA)> <!ELEMENT sourcebank_ref (#PCDATA)> <!ATTLIST sequence_parameter %I_Pname;> <!ELEMENT and_selection (selection_metadata | and_selection | or_selection | not_selection)+> <!ELEMENT or_selection (selection_metadata | and_selection | or_selection | not_selection)+> <!ELEMENT not_selection (selection_metadata | and_selection | or_selection | not_selection)> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- OUTCOMES PREOCESSING OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT objects_condition (qticomment? , (outcomes_metadata | and_objects | or_objects | not_objects)? , objects_parameter* , map_input* , objectscond_extension?)> <!ELEMENT map_output (#PCDATA)> <!ATTLIST map_output %I_VarName;> <!ELEMENT map_input (#PCDATA)> <!ATTLIST map_input %I_VarName;> <!ELEMENT outcomes_feedback_test (test_variable , displayfeedback+)> <!ATTLIST outcomes_feedback_test %I_Title; > <!ELEMENT outcomes_metadata (#PCDATA)> <!ATTLIST outcomes_metadata %I_Mdname; %I_Mdoperator;> <!ELEMENT and_objects (outcomes_metadata | and_objects | or_objects | not_objects)+> <!ELEMENT or_objects (outcomes_metadata | and_objects | or_objects | not_objects)+> <!ELEMENT not_objects (outcomes_metadata | and_objects | or_objects | not_objects)> <!ELEMENT test_variable (variable_test | and_test | or_test | not_test)> <!ELEMENT processing_parameter (#PCDATA)> <!ATTLIST processing_parameter %I_Pname;> <!ELEMENT and_test (variable_test | and_test | or_test | not_test)+> <!ELEMENT or_test (variable_test | and_test | or_test | not_test)+> <!ELEMENT not_test (variable_test | and_test | or_test | not_test)> <!ELEMENT variable_test (#PCDATA)> <!ATTLIST variable_test %I_VarName; %I_Testoperator;> <!ELEMENT objects_parameter (#PCDATA)> <!ATTLIST objects_parameter %I_Pname;> \ No newline at end of file diff --git a/src/main/java/org/olat/ims/resources/dtd/ims_qtiresv1p2.dtd b/src/main/java/org/olat/ims/resources/dtd/ims_qtiresv1p2.dtd new file mode 100644 index 00000000000..3c6fa40423d --- /dev/null +++ b/src/main/java/org/olat/ims/resources/dtd/ims_qtiresv1p2.dtd @@ -0,0 +1 @@ +<?xml version='1.0' encoding='UTF-8' ?> <!--Generated by XML Authority--> <!-- ******************************************************* --> <!-- --> <!-- TITLE: ims_qtiresv1p2.dtd --> <!-- TYPE: IMS Question and Test Interoperability --> <!-- Results Reporting --> <!-- --> <!-- REVISION HISTORY: --> <!-- Date Author --> <!-- ==== ====== --> <!-- 922th Jan 2002 Colin Smythe --> <!-- --> <!-- This specification has been approved as a Final release. --> <!-- --> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- ENTITY DEFINITIONS --> <!-- ******************************************************* --> <!ENTITY % I_Vartype " vartype (Integer | Decimal | Scientific | String | Boolean | Set | Enumerated ) 'Integer'"> <!ENTITY % I_Varname " varname CDATA 'SCORE'"> <!ENTITY % I_Identref " ident_ref CDATA #IMPLIED"> <!ENTITY % I_Evaltitle " asi_title CDATA #IMPLIED"> <!ENTITY % I_Presented " presented (Yes | No | Unknown ) 'Yes'"> <!ENTITY % I_Status " status (Valid | Noanswer | Error ) 'Valid'"> <!ENTITY % I_Uri " uri CDATA #IMPLIED"> <!ENTITY % I_Entityref " entityref ENTITY #IMPLIED"> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- ROOT DEFINITION --> <!-- ******************************************************* --> <!ELEMENT qti_result_report (qti_comment? , result+)> <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!ELEMENT qti_comment (#PCDATA)> <!ATTLIST qti_comment xml:lang CDATA #IMPLIED > <!-- ******************************************************* --> <!-- CORE OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT result (qti_comment? , context , (summary_result | assessment_result | section_result | item_result) , extension_result?)> <!ELEMENT context (qti_comment? , name? , generic_identifier* , date* , extension_context?)> <!ELEMENT summary_result (qti_comment? , type_label? , generic_identifier* , date* , status? , duration? , score? , grade? , outcomes? , extension_summary_result?)> <!ATTLIST summary_result %I_Evaltitle; > <!ELEMENT assessment_result (qti_comment? , asi_metadata* , asi_description? , date* , duration? , objective* , control? , outcomes? , feedback_displayed* , num_items? , num_sections? , num_items_presented? , num_items_attempted? , num_sections_presented? , section_result* , extension_assessment_result?)> <!ATTLIST assessment_result %I_Identref; %I_Evaltitle; > <!ELEMENT section_result (qti_comment? , asi_metadata* , asi_description? , date* , duration? , objective* , control? , outcomes? , feedback_displayed* , num_items? , num_sections? , num_items_presented? , num_items_attempted? , num_sections_presented? , (section_result | item_result)* , extension_section_result?)> <!ATTLIST section_result %I_Evaltitle; %I_Identref; %I_Presented; > <!ELEMENT item_result (qti_comment? , asi_metadata* , asi_description? , date* , duration? , objective* , control? , response* , outcomes? , feedback_displayed* , extension_item_result?)> <!ATTLIST item_result %I_Evaltitle; %I_Identref; %I_Presented; > <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> <!-- ******************************************************* --> <!-- SUPPORT OBJECT DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT name (#PCDATA)> <!ELEMENT generic_identifier (type_label? , identifier_string)> <!ELEMENT date (type_label? , datetime)> <!ELEMENT score_min (#PCDATA)> <!ELEMENT score_max (#PCDATA)> <!ELEMENT score_value (#PCDATA)> <!ELEMENT grade (grade_value , grade_cut? , extension_grade?)> <!ATTLIST grade %I_Varname; %I_Status; members CDATA #IMPLIED > <!ELEMENT duration (#PCDATA)> <!ELEMENT type_label (#PCDATA)> <!ATTLIST type_label source CDATA #IMPLIED > <!ELEMENT status (type_label? , datetime? , status_value)> <!ELEMENT objective (#PCDATA)> <!ATTLIST objective view (All | Administrator | AdminAuthority | Assessor | Author | Candidate | InvigilatorProctor | Psychometrician | Scorer | Tutor ) 'All' %I_Uri; %I_Entityref; > <!ELEMENT control (#PCDATA)> <!ATTLIST control hint_switch (Yes | No ) 'No' solution_switch (Yes | No ) 'No' feedback_switch (Yes | No ) 'No' > <!ELEMENT asi_metadata (vocabulary? , asi_metadatafield+)> <!ELEMENT response (qti_comment? , response_form? , num_attempts? , response_value* , extension_response?)> <!ATTLIST response %I_Identref; > <!ELEMENT response_value (#PCDATA)> <!ATTLIST response_value %I_Uri; %I_Entityref; response_status (Null | Valid | NA | Invalid ) 'Valid' response_time CDATA #IMPLIED > <!ELEMENT feedback_displayed (#PCDATA)> <!ATTLIST feedback_displayed %I_Uri; %I_Entityref; %I_Identref; %I_Evaltitle; > <!ELEMENT num_items (#PCDATA)> <!ELEMENT num_sections (#PCDATA)> <!ELEMENT num_sections_presented (#PCDATA)> <!ELEMENT num_items_attempted (#PCDATA)> <!ELEMENT num_items_presented (#PCDATA)> <!ELEMENT response_form (correct_response* , extension_responseform?)> <!ATTLIST response_form cardinality (single | multiple | ordered | extension ) #IMPLIED render_type (choice | hotspot | slider | fib | extension ) #IMPLIED timing (Yes | No ) #IMPLIED response_type (lid | xy | str | num | grp | extension ) #IMPLIED > <!ELEMENT correct_response (#PCDATA)> <!ELEMENT outcomes (status? , (score | grade)*)> <!ELEMENT score (score_value , score_interpretation? , score_min? , score_max? , score_normalized? , score_average? , score_std_error? , score_reliability? , score_cut? , extension_score?)> <!ATTLIST score %I_Varname; %I_Vartype; %I_Status; > <!ELEMENT score_cut (#PCDATA)> <!ELEMENT grade_value (#PCDATA)> <!ELEMENT grade_cut (#PCDATA)> <!ELEMENT score_average (#PCDATA)> <!ELEMENT score_std_error (#PCDATA)> <!ELEMENT num_attempts (#PCDATA)> <!ELEMENT asi_description (#PCDATA)> <!ATTLIST asi_description %I_Uri; %I_Entityref; > <!ELEMENT identifier_string (#PCDATA)> <!ELEMENT datetime (#PCDATA)> <!ELEMENT status_value (#PCDATA)> <!ELEMENT vocabulary (#PCDATA)> <!ATTLIST vocabulary %I_Uri; %I_Entityref; vocab_type CDATA #IMPLIED > <!ELEMENT asi_metadatafield (field_name , field_value)> <!ATTLIST asi_metadatafield xml:lang CDATA #IMPLIED > <!ELEMENT field_name (#PCDATA)> <!ELEMENT field_value (#PCDATA)> <!ELEMENT score_reliability (#PCDATA)> <!ELEMENT score_interpretation (#PCDATA)> <!ATTLIST score_interpretation %I_Uri; %I_Entityref; > <!-- ******************************************************* --> <!-- EXTENSION DEFINITIONS --> <!-- ******************************************************* --> <!ELEMENT extension_context ANY> <!ELEMENT extension_assessment_result ANY> <!ELEMENT extension_section_result ANY> <!ELEMENT extension_score ANY> <!ELEMENT extension_summary_result ANY> <!ELEMENT extension_item_result ANY> <!ELEMENT extension_response ANY> <!ELEMENT extension_responseform ANY> <!ELEMENT extension_grade ANY> <!ELEMENT extension_result ANY> <!ELEMENT score_normalized (#PCDATA)> \ No newline at end of file diff --git a/src/main/java/org/olat/modules/qpool/QuestionItem.java b/src/main/java/org/olat/modules/qpool/QuestionItem.java index 3fe974ff063..ed6057940ed 100644 --- a/src/main/java/org/olat/modules/qpool/QuestionItem.java +++ b/src/main/java/org/olat/modules/qpool/QuestionItem.java @@ -37,6 +37,7 @@ public interface QuestionItem extends QuestionItemShort { public String getLevel(); + public String getDirectory(); public String getEditor(); diff --git a/src/main/java/org/olat/modules/qpool/QuestionItemShort.java b/src/main/java/org/olat/modules/qpool/QuestionItemShort.java index fe8aad00a21..b53b5c8090a 100644 --- a/src/main/java/org/olat/modules/qpool/QuestionItemShort.java +++ b/src/main/java/org/olat/modules/qpool/QuestionItemShort.java @@ -34,6 +34,8 @@ public interface QuestionItemShort extends OLATResourceable { public Long getKey(); + public String getUuid(); + public Date getCreationDate(); public Date getLastModified(); diff --git a/src/main/java/org/olat/modules/qpool/QuestionPoolModule.java b/src/main/java/org/olat/modules/qpool/QuestionPoolModule.java index 15b9e5e2278..9f4485b891f 100644 --- a/src/main/java/org/olat/modules/qpool/QuestionPoolModule.java +++ b/src/main/java/org/olat/modules/qpool/QuestionPoolModule.java @@ -19,12 +19,18 @@ */ package org.olat.modules.qpool; +import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl; import org.olat.core.configuration.AbstractOLATModule; import org.olat.core.configuration.ConfigOnOff; import org.olat.core.configuration.PersistedProperties; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.modules.qpool.impl.PdfQuestionPoolServiceProvider; +import org.olat.modules.qpool.impl.TextQuestionPoolServiceProvider; /** * @@ -36,9 +42,12 @@ public class QuestionPoolModule extends AbstractOLATModule implements ConfigOnOf private final List<QuestionPoolSPI> questionPoolProviders = new ArrayList<QuestionPoolSPI>(); + private VFSContainer rootContainer; + @Override public void init() { - // + addQuestionPoolProvider(new TextQuestionPoolServiceProvider()); + addQuestionPoolProvider(new PdfQuestionPoolServiceProvider()); } @Override @@ -60,14 +69,25 @@ public class QuestionPoolModule extends AbstractOLATModule implements ConfigOnOf public boolean isEnabled() { return true; } + + public VFSContainer getRootContainer() { + if(rootContainer == null) { + rootContainer = new OlatRootFolderImpl(File.separator + "qpool", null); + } + return rootContainer; + } public List<QuestionPoolSPI> getQuestionPoolProviders() { - return new ArrayList<QuestionPoolSPI>(questionPoolProviders); + List<QuestionPoolSPI> providers = new ArrayList<QuestionPoolSPI>(questionPoolProviders); + Collections.sort(providers, new QuestionPoolSPIComparator()); + return providers; } public void setQuestionPoolProviders(List<QuestionPoolSPI> providers) { if(providers != null) { - questionPoolProviders.addAll(providers); + for(QuestionPoolSPI provider:providers) { + addQuestionPoolProvider(provider); + } } } @@ -81,8 +101,19 @@ public class QuestionPoolModule extends AbstractOLATModule implements ConfigOnOf } public void addQuestionPoolProvider(QuestionPoolSPI provider) { - questionPoolProviders.add(provider); + int currentIndex = -1; + for(int i=questionPoolProviders.size(); i-->0; ) { + QuestionPoolSPI currentProvider = questionPoolProviders.get(i); + if(provider.getFormat() != null && + provider.getFormat().equals(currentProvider.getFormat())) { + currentIndex = i; + } + } + + if(currentIndex >= 0) { + questionPoolProviders.set(currentIndex, provider); + } else { + questionPoolProviders.add(provider); + } } - - } diff --git a/src/main/java/org/olat/modules/qpool/QuestionPoolSPI.java b/src/main/java/org/olat/modules/qpool/QuestionPoolSPI.java index 82099f5bfe9..877e85b0667 100644 --- a/src/main/java/org/olat/modules/qpool/QuestionPoolSPI.java +++ b/src/main/java/org/olat/modules/qpool/QuestionPoolSPI.java @@ -19,9 +19,12 @@ */ package org.olat.modules.qpool; +import java.io.File; + import org.olat.core.gui.UserRequest; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; +import org.olat.core.util.vfs.VFSLeaf; /** * @@ -31,8 +34,14 @@ import org.olat.core.gui.control.WindowControl; */ public interface QuestionPoolSPI { + public int getPriority(); + public String getFormat(); + public boolean isCompatible(String filename, File file); + + public boolean isCompatible(String filename, VFSLeaf file); + public Controller getPreviewController(UserRequest ureq, WindowControl wControl, QuestionItem item); public Controller getEditableController(UserRequest ureq, WindowControl wControl, QuestionItem item); diff --git a/src/main/java/org/olat/modules/qpool/QuestionPoolSPIComparator.java b/src/main/java/org/olat/modules/qpool/QuestionPoolSPIComparator.java new file mode 100644 index 00000000000..69f7f0f5890 --- /dev/null +++ b/src/main/java/org/olat/modules/qpool/QuestionPoolSPIComparator.java @@ -0,0 +1,38 @@ +/** + * <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.modules.qpool; + +import java.util.Comparator; + +/** + * Sort by priority + * Initial date: 27.02.2013<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class QuestionPoolSPIComparator implements Comparator<QuestionPoolSPI> { + + @Override + public int compare(QuestionPoolSPI o1, QuestionPoolSPI o2) { + int p1 = o1.getPriority(); + int p2 = o2.getPriority(); + return p1 < p2 ? 1 : (p1==p2 ? 0 : -1); + } +} diff --git a/src/main/java/org/olat/modules/qpool/QuestionPoolService.java b/src/main/java/org/olat/modules/qpool/QuestionPoolService.java index 45a4c105d8c..43819bf5310 100644 --- a/src/main/java/org/olat/modules/qpool/QuestionPoolService.java +++ b/src/main/java/org/olat/modules/qpool/QuestionPoolService.java @@ -19,10 +19,13 @@ */ package org.olat.modules.qpool; +import java.io.File; import java.util.List; import org.olat.core.commons.persistence.SortKey; import org.olat.core.id.Identity; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSLeaf; import org.olat.group.BusinessGroup; import org.olat.resource.OLATResource; @@ -40,6 +43,12 @@ public interface QuestionPoolService { public void addAuthors(List<Identity> authors, List<QuestionItem> items); + public QuestionItem importItem(Identity owner, String filename, File file); + + public VFSLeaf getRootFile(QuestionItem item); + + public VFSContainer getRootDirectory(QuestionItem item); + public int countItems(Identity author); diff --git a/src/main/java/org/olat/modules/qpool/impl/PdfQuestionPoolServiceProvider.java b/src/main/java/org/olat/modules/qpool/impl/PdfQuestionPoolServiceProvider.java new file mode 100644 index 00000000000..87e64c5631b --- /dev/null +++ b/src/main/java/org/olat/modules/qpool/impl/PdfQuestionPoolServiceProvider.java @@ -0,0 +1,76 @@ +/** + * <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.modules.qpool.impl; + +import java.io.File; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.modules.qpool.QuestionItem; +import org.olat.modules.qpool.QuestionPoolSPI; + +/** + * + * Initial date: 26.02.2013<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class PdfQuestionPoolServiceProvider implements QuestionPoolSPI { + + public static final String PDF_FORMAT = "pdf"; + + @Override + public int getPriority() { + return 0; + } + + @Override + public String getFormat() { + return PDF_FORMAT; + } + + @Override + public boolean isCompatible(String filename, File file) { + return !filename.toLowerCase().endsWith(".xml") + && !filename.toLowerCase().endsWith(".txt") + && !filename.toLowerCase().endsWith(".zip"); + } + + @Override + public boolean isCompatible(String filename, VFSLeaf file) { + return isCompatible(filename, (File)null); + } + + @Override + public Controller getPreviewController(UserRequest ureq, WindowControl wControl, QuestionItem item) { + return null; + } + + @Override + public Controller getEditableController(UserRequest ureq, WindowControl wControl, QuestionItem item) { + return null; + } + + + + +} diff --git a/src/main/java/org/olat/modules/qpool/impl/TextQuestionPoolServiceProvider.java b/src/main/java/org/olat/modules/qpool/impl/TextQuestionPoolServiceProvider.java new file mode 100644 index 00000000000..efd6b386755 --- /dev/null +++ b/src/main/java/org/olat/modules/qpool/impl/TextQuestionPoolServiceProvider.java @@ -0,0 +1,70 @@ +/** + * <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.modules.qpool.impl; + +import java.io.File; + +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.modules.qpool.QuestionItem; +import org.olat.modules.qpool.QuestionPoolSPI; + +/** + * + * Initial date: 26.02.2013<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class TextQuestionPoolServiceProvider implements QuestionPoolSPI { + + public static final String TXT_FORMAT = "txt"; + + @Override + public int getPriority() { + return 1; + } + + @Override + public String getFormat() { + return TXT_FORMAT; + } + + @Override + public boolean isCompatible(String filename, File file) { + return filename.toLowerCase().endsWith(".txt"); + } + + @Override + public boolean isCompatible(String filename, VFSLeaf file) { + return isCompatible(filename, (File)null); + } + + @Override + public Controller getPreviewController(UserRequest ureq, WindowControl wControl, QuestionItem item) { + return null; + } + + @Override + public Controller getEditableController(UserRequest ureq, WindowControl wControl, QuestionItem item) { + return null; + } +} diff --git a/src/main/java/org/olat/modules/qpool/manager/NullPoolService.java b/src/main/java/org/olat/modules/qpool/manager/NullPoolService.java index a1be17f554d..685691003ba 100644 --- a/src/main/java/org/olat/modules/qpool/manager/NullPoolService.java +++ b/src/main/java/org/olat/modules/qpool/manager/NullPoolService.java @@ -94,7 +94,7 @@ public class NullPoolService implements ApplicationListener<ContextRefreshedEven for(int i=0; i<200; i++) { long randomIndex = Math.round(Math.random() * (fields.size() - 1)); StudyField field = fields.get((int)randomIndex); - QuestionItem item = questionItemDao.create(null, "NGC " + i, QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), field, randomType()); + QuestionItem item = questionItemDao.create(null, "NGC " + i, QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), field, null, randomType()); poolDao.addItemToPool(item, pools.get(0)); } } 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 2375e92a533..935de6092c2 100644 --- a/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java +++ b/src/main/java/org/olat/modules/qpool/manager/QuestionItemDAO.java @@ -22,6 +22,7 @@ package org.olat.modules.qpool.manager; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.UUID; import javax.persistence.EntityManager; import javax.persistence.LockModeType; @@ -59,18 +60,33 @@ public class QuestionItemDAO { @Autowired private BaseSecurity securityManager; + public QuestionItem create(Identity owner, String subject, String format, String language, - StudyField field, QuestionType type) { + StudyField field, String rootFilename, QuestionType type) { + + String uuid = UUID.randomUUID().toString(); + return create(owner, subject, format, language, field, uuid, rootFilename, type) ; + } + + public QuestionItem create(Identity owner, String subject, String format, String language, + StudyField field, String uuid, String rootFilename, QuestionType type) { QuestionItemImpl item = new QuestionItemImpl(); + + + item.setUuid(uuid); item.setCreationDate(new Date()); item.setLastModified(new Date()); item.setSubject(subject); item.setStatus(QuestionStatus.inWork.name()); item.setUsage(0); - item.setType(type.name()); + if(type != null) { + item.setType(type.name()); + } item.setFormat(format); item.setLanguage(language); item.setStudyField(field); + item.setDirectory(generateDir(uuid)); + item.setRootFilename(rootFilename); SecurityGroup authorGroup = securityManager.createAndPersistSecurityGroup(); item.setAuthorGroup(authorGroup); dbInstance.getCurrentEntityManager().persist(item); @@ -80,6 +96,21 @@ public class QuestionItemDAO { return item; } + public String generateDir(String uuid) { + String cleanUuid = uuid.replace("-", ""); + String firstToken = cleanUuid.substring(0, 2); + String secondToken = cleanUuid.substring(2, 4); + String thirdToken = cleanUuid.substring(4, 6); + String forthToken = cleanUuid.substring(6, 8); + StringBuilder sb = new StringBuilder(); + sb.append(firstToken).append("/") + .append(secondToken).append("/") + .append(thirdToken).append("/") + .append(forthToken).append("/"); + return sb.toString(); + } + + public void addAuthors(List<Identity> authors, QuestionItem item) { QuestionItemImpl lockedItem = loadForUpdate(item.getKey()); SecurityGroup secGroup = lockedItem.getAuthorGroup(); @@ -149,11 +180,11 @@ public class QuestionItemDAO { } } - public QuestionItem loadById(Long key) { + public QuestionItemImpl loadById(Long key) { StringBuilder sb = new StringBuilder(); sb.append("select item from questionitem item where item.key=:key"); - List<QuestionItem> items = dbInstance.getCurrentEntityManager() - .createQuery(sb.toString(), QuestionItem.class) + List<QuestionItemImpl> items = dbInstance.getCurrentEntityManager() + .createQuery(sb.toString(), QuestionItemImpl.class) .setParameter("key", key) .getResultList(); 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 c7cb6c619c2..aea658ec407 100644 --- a/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java +++ b/src/main/java/org/olat/modules/qpool/manager/QuestionPoolServiceImpl.java @@ -19,16 +19,32 @@ */ package org.olat.modules.qpool.manager; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.UUID; +import org.apache.commons.io.IOUtils; import org.olat.core.commons.persistence.DB; import org.olat.core.commons.persistence.SortKey; import org.olat.core.id.Identity; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; +import org.olat.core.util.ZipUtil; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSLeaf; import org.olat.group.BusinessGroup; import org.olat.modules.qpool.Pool; import org.olat.modules.qpool.QuestionItem; import org.olat.modules.qpool.QuestionItemCollection; +import org.olat.modules.qpool.QuestionPoolModule; +import org.olat.modules.qpool.QuestionPoolSPI; import org.olat.modules.qpool.QuestionPoolService; import org.olat.modules.qpool.model.QuestionItemImpl; import org.olat.resource.OLATResource; @@ -44,6 +60,8 @@ import org.springframework.stereotype.Service; @Service("qpoolService") public class QuestionPoolServiceImpl implements QuestionPoolService { + private static final OLog log = Tracing.createLoggerFor(QuestionPoolServiceImpl.class); + @Autowired private DB dbInstance; @Autowired @@ -54,6 +72,9 @@ public class QuestionPoolServiceImpl implements QuestionPoolService { private StudyFieldDAO studyFieldDao; @Autowired private QuestionItemDAO questionItemDao; + @Autowired + private QuestionPoolModule qpoolModule; + @Override public String getMateriliazedPathOfStudyFields(QuestionItem item) { @@ -84,6 +105,113 @@ public class QuestionPoolServiceImpl implements QuestionPoolService { } } + @Override + public QuestionItem importItem(Identity owner, String filename, File file) { + QuestionItem importedItem = null; + List<QuestionPoolSPI> providers = qpoolModule.getQuestionPoolProviders(); + for(QuestionPoolSPI provider:providers) { + if(provider.isCompatible(filename, file)) { + importedItem = importItem(owner, filename, file, provider); + } + } + return importedItem; + } + + @Override + public VFSLeaf getRootFile(QuestionItem item) { + QuestionItemImpl reloadedItem = questionItemDao.loadById(item.getKey()); + VFSContainer root = qpoolModule.getRootContainer(); + VFSItem dir = root.resolve(reloadedItem.getDirectory()); + if(dir instanceof VFSContainer) { + VFSContainer itemContainer = (VFSContainer)dir; + VFSItem rootLeaf = itemContainer.resolve(reloadedItem.getRootFilename()); + if(rootLeaf instanceof VFSLeaf) { + return (VFSLeaf)rootLeaf; + } + } + return null; + } + + @Override + public VFSContainer getRootDirectory(QuestionItem item) { + QuestionItemImpl reloadedItem = questionItemDao.loadById(item.getKey()); + VFSContainer root = qpoolModule.getRootContainer(); + VFSItem dir = root.resolve(reloadedItem.getDirectory()); + if(dir instanceof VFSContainer) { + return (VFSContainer)dir; + } + return null; + } + + private QuestionItem importItem(Identity owner, String filename, File file, QuestionPoolSPI provider) { + String uuid = UUID.randomUUID().toString(); + VFSContainer root = qpoolModule.getRootContainer(); + VFSContainer itemDir = getDirectory(root, uuid); + + String rootFilename = filename; + if(filename.toLowerCase().endsWith(".zip")) { + ZipUtil.unzipStrict(file, itemDir); + rootFilename = searchRootFilename("", itemDir, provider); + } else { + //copy + VFSLeaf leaf = itemDir.createChildLeaf(filename); + OutputStream out = leaf.getOutputStream(false); + InputStream in = null; + try { + in = new FileInputStream(file); + IOUtils.copy(in, out); + } catch (FileNotFoundException e) { + log.error("", e); + } catch (IOException e) { + log.error("", e); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + } + } + + return questionItemDao.create(owner, filename, provider.getFormat(), "de", null, uuid, rootFilename, null); + } + + private String searchRootFilename(String path, VFSContainer dir, QuestionPoolSPI provider) { + for(VFSItem item:dir.getItems()) { + if(item instanceof VFSContainer) { + String root = searchRootFilename(path + "/" + item.getName(), (VFSContainer)item, provider); + if(root != null) { + return root; + } + } else if(item instanceof VFSLeaf) { + if(provider.isCompatible(item.getName(), (VFSLeaf)item)) { + return path + item.getName(); + } + } + } + return null; + } + + private VFSContainer getDirectory(VFSContainer rootContainer, String uuid) { + String cleanUuid = uuid.replace("-", ""); + String firstToken = cleanUuid.substring(0, 2); + VFSContainer firstContainer = getNextDirectory(rootContainer, firstToken); + String secondToken = cleanUuid.substring(2, 4); + VFSContainer secondContainer = getNextDirectory(firstContainer, secondToken); + String thirdToken = cleanUuid.substring(4, 6); + VFSContainer thridContainer = getNextDirectory(secondContainer, thirdToken); + String forthToken = cleanUuid.substring(6, 8); + return getNextDirectory(thridContainer, forthToken); + } + + private VFSContainer getNextDirectory(VFSContainer container, String token) { + VFSItem nextContainer = container.resolve(token); + if(nextContainer instanceof VFSContainer) { + return (VFSContainer)nextContainer; + } else if (nextContainer instanceof VFSLeaf) { + log.error(""); + return null; + } + return container.createChildContainer(token); + } + @Override public int countItems(Identity author) { return questionItemDao.countItems(author); diff --git a/src/main/java/org/olat/modules/qpool/manager/StudyFieldDAO.java b/src/main/java/org/olat/modules/qpool/manager/StudyFieldDAO.java index 6d89ea04122..160ce247c7b 100644 --- a/src/main/java/org/olat/modules/qpool/manager/StudyFieldDAO.java +++ b/src/main/java/org/olat/modules/qpool/manager/StudyFieldDAO.java @@ -87,6 +87,10 @@ public class StudyFieldDAO { } public String getMaterializedPath(StudyField field) { + if(field == null) { + return ""; + } + List<StudyField> parentLine = new ArrayList<StudyField>(); StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java b/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java index 7d499ccb7f7..af13a894d17 100644 --- a/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java +++ b/src/main/java/org/olat/modules/qpool/model/QuestionItemImpl.java @@ -67,6 +67,8 @@ public class QuestionItemImpl implements QuestionItem, CreateInfo, ModifiedInfo, @GenericGenerator(name = "system-uuid", strategy = "hilo") @Column(name="id", nullable=false, unique=true, insertable=true, updatable=false) private Long key; + @Column(name="q_uuid", nullable=false, insertable=true, updatable=false) + private String uuid; //general @Column(name="q_subject", nullable=false, insertable=true, updatable=true) @@ -119,8 +121,12 @@ public class QuestionItemImpl implements QuestionItem, CreateInfo, ModifiedInfo, private Date lastModified; @Column(name="q_version", nullable=true, insertable=true, updatable=true) private String itemVersion; - - + + @Column(name="q_dir", nullable=true, insertable=true, updatable=false) + private String directory; + @Column(name="q_root_filename", nullable=true, insertable=true, updatable=false) + private String rootFilename; + @Override public Long getKey() { @@ -131,6 +137,15 @@ public class QuestionItemImpl implements QuestionItem, CreateInfo, ModifiedInfo, this.key = key; } + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + @Override public String getResourceableTypeName() { return "QuestionItem"; @@ -320,6 +335,22 @@ public class QuestionItemImpl implements QuestionItem, CreateInfo, ModifiedInfo, this.itemVersion = itemVersion; } + public String getDirectory() { + return directory; + } + + public void setDirectory(String directory) { + this.directory = directory; + } + + public String getRootFilename() { + return rootFilename; + } + + public void setRootFilename(String rootFilename) { + this.rootFilename = rootFilename; + } + @Override public int hashCode() { return key == null ? 97489 : key.hashCode(); diff --git a/src/main/java/org/olat/modules/qpool/ui/ImportQuestionItemController.java b/src/main/java/org/olat/modules/qpool/ui/ImportQuestionItemController.java new file mode 100644 index 00000000000..1e305b301a1 --- /dev/null +++ b/src/main/java/org/olat/modules/qpool/ui/ImportQuestionItemController.java @@ -0,0 +1,84 @@ +/** + * <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.modules.qpool.ui; + +import java.io.File; + +import org.olat.core.CoreSpringFactory; +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.FileElement; +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.modules.qpool.QuestionPoolService; + +/** + * + * Initial date: 26.02.2013<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class ImportQuestionItemController extends FormBasicController { + + private FileElement fileEl; + private final QuestionPoolService qpoolservice; + + public ImportQuestionItemController(UserRequest ureq, WindowControl wControl) { + super(ureq, wControl); + qpoolservice = CoreSpringFactory.getImpl(QuestionPoolService.class); + initForm(ureq); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + fileEl = uifactory.addFileElement("item", "import.item", formLayout); + + FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); + buttonsCont.setRootForm(mainForm); + formLayout.add(buttonsCont); + uifactory.addFormSubmitButton("ok", "ok", buttonsCont); + uifactory.addFormCancelButton("cancel", buttonsCont, ureq, getWindowControl()); + } + + @Override + protected void doDispose() { + // + } + + public File getFile() { + return fileEl.getUploadFile(); + } + + @Override + protected void formOK(UserRequest ureq) { + String filename = fileEl.getUploadFileName(); + File file = fileEl.getUploadFile(); + qpoolservice.importItem(getIdentity(), filename, file); + fireEvent(ureq, Event.DONE_EVENT); + } + + @Override + protected void formCancelled(UserRequest ureq) { + fireEvent(ureq, Event.CANCELLED_EVENT); + } +} diff --git a/src/main/java/org/olat/modules/qpool/ui/QuestionItemRow.java b/src/main/java/org/olat/modules/qpool/ui/QuestionItemRow.java index 48ae0df36c4..026b4f3cd10 100644 --- a/src/main/java/org/olat/modules/qpool/ui/QuestionItemRow.java +++ b/src/main/java/org/olat/modules/qpool/ui/QuestionItemRow.java @@ -48,6 +48,11 @@ public class QuestionItemRow implements QuestionItemShort { return delegate.getKey(); } + @Override + public String getUuid() { + return delegate.getUuid(); + } + @Override public String getResourceableTypeName() { return delegate.getResourceableTypeName(); 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 3d859aefc49..142661ae58e 100644 --- a/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java +++ b/src/main/java/org/olat/modules/qpool/ui/QuestionListController.java @@ -69,7 +69,7 @@ import org.olat.modules.qpool.ui.QuestionItemDataModel.Cols; */ public class QuestionListController extends FormBasicController implements StackedControllerAware, ItemRowsSource { - private FormLink createList, shareItem, deleteItem, authorItem; + private FormLink createList, shareItem, deleteItem, authorItem, importItem; private FlexiTableElement itemsTable; private QuestionItemDataModel model; @@ -80,6 +80,7 @@ public class QuestionListController extends FormBasicController implements Stack private SelectBusinessGroupController selectGroupCtrl; private CreateCollectionController createCollectionCtrl; private StepsMainRunController importAuthorsWizard; + private ImportQuestionItemController importItemCtrl; private final MarkManager markManager; private final QuestionPoolService qpoolService; @@ -121,6 +122,7 @@ public class QuestionListController extends FormBasicController implements Stack createList = uifactory.addFormLink("create.list", formLayout, Link.BUTTON); shareItem = uifactory.addFormLink("share.item", formLayout, Link.BUTTON); + importItem = uifactory.addFormLink("import.item", formLayout, Link.BUTTON); authorItem = uifactory.addFormLink("author.item", formLayout, Link.BUTTON); deleteItem = uifactory.addFormLink("delete.item", formLayout, Link.BUTTON); } @@ -170,6 +172,8 @@ public class QuestionListController extends FormBasicController implements Stack List<QuestionItem> items = getQuestionItems(selections); doChooseAuthoren(ureq, items); } + } else if(link == importItem) { + doOpenImport(ureq); } else if("select".equals(link.getCmd())) { QuestionItemRow row = (QuestionItemRow)link.getUserObject(); doSelect(ureq, row.getItem()); @@ -226,6 +230,12 @@ public class QuestionListController extends FormBasicController implements Stack removeAsListenerAndDispose(importAuthorsWizard); importAuthorsWizard = null; } + } else if(source == importItemCtrl) { + if(event == Event.DONE_EVENT || event == Event.CHANGED_EVENT) { + // + } + cmc.deactivate(); + cleanUp(); } else if(source == confirmDeleteBox) { boolean delete = DialogBoxUIFactory.isYesEvent(event) || DialogBoxUIFactory.isOkEvent(event); if(delete) { @@ -241,9 +251,11 @@ public class QuestionListController extends FormBasicController implements Stack private void cleanUp() { removeAsListenerAndDispose(cmc); + removeAsListenerAndDispose(importItemCtrl); removeAsListenerAndDispose(selectGroupCtrl); removeAsListenerAndDispose(createCollectionCtrl); cmc = null; + importItemCtrl = null; selectGroupCtrl = null; createCollectionCtrl = null; } @@ -267,6 +279,17 @@ public class QuestionListController extends FormBasicController implements Stack return null; } + private void doOpenImport(UserRequest ureq) { + removeAsListenerAndDispose(importItemCtrl); + importItemCtrl = new ImportQuestionItemController(ureq, getWindowControl()); + listenTo(importItemCtrl); + + cmc = new CloseableModalController(getWindowControl(), translate("close"), + importItemCtrl.getInitialComponent(), true, translate("import.item")); + cmc.activate(); + listenTo(cmc); + } + private void doAskCollectionName(UserRequest ureq, List<QuestionItem> items) { removeAsListenerAndDispose(createCollectionCtrl); createCollectionCtrl = new CreateCollectionController(ureq, getWindowControl()); diff --git a/src/main/java/org/olat/modules/qpool/ui/_content/item_list.html b/src/main/java/org/olat/modules/qpool/ui/_content/item_list.html index 5037cd65133..964db69037e 100644 --- a/src/main/java/org/olat/modules/qpool/ui/_content/item_list.html +++ b/src/main/java/org/olat/modules/qpool/ui/_content/item_list.html @@ -3,6 +3,7 @@ $r.render("items") <div class="o_qpool_button_bar"> $r.render("create.list") $r.render("share.item") + $r.render("import.item") $r.render("author.item") $r.render("delete.item") </div> diff --git a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties index b7cbb05ecb1..ae0b8e6fb11 100644 --- a/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/modules/qpool/ui/_i18n/LocalStrings_de.properties @@ -66,4 +66,5 @@ author.confirm.title=Best table.user.login=$org.olat.admin.user\:table.user.login share.item=Freigeben select.group=Freigeben -select.item=Bearbeiten \ No newline at end of file +select.item=Bearbeiten +import.item=Import \ No newline at end of file diff --git a/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql b/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql index bdeb81e0ec9..308df5e13cb 100644 --- a/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql +++ b/src/main/resources/database/mysql/alter_8_4_0_to_9_0_0.sql @@ -26,10 +26,11 @@ create table if not exists o_qp_study_field ( create table if not exists o_qp_item ( id bigint not null, + q_uuid varchar(36) not null, q_subject varchar(255) not null, q_keywords varchar(2048), - q_type varchar(64) not null, - q_language varchar(16) not null, + q_type varchar(64), + q_language varchar(16), q_status varchar(32) not null, q_description varchar(4000), q_copyright varchar(2048), @@ -44,6 +45,8 @@ create table if not exists o_qp_item ( creationdate datetime not null, lastmodified datetime not null, q_version varchar(32), + q_dir varchar(32), + q_root_filename varchar(255), fk_study_field bigint, fk_author_grp_id bigint, primary key (id) diff --git a/src/test/java/org/olat/modules/ims/qti/fileresource/FileResourceValidatorTest.java b/src/test/java/org/olat/modules/ims/qti/fileresource/FileResourceValidatorTest.java new file mode 100644 index 00000000000..c28b7e1eb55 --- /dev/null +++ b/src/test/java/org/olat/modules/ims/qti/fileresource/FileResourceValidatorTest.java @@ -0,0 +1,46 @@ +package org.olat.modules.ims.qti.fileresource; + +import static org.junit.Assert.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; + +import junit.framework.Assert; + +import org.junit.Test; +import org.olat.ims.qti.fileresource.ItemFileResourceValidator; + +/** + * + * Initial date: 27.02.2013<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class FileResourceValidatorTest { + + @Test + public void testItemValidation_xml() throws IOException, URISyntaxException { + URL itemUrl = FileResourceValidatorTest.class.getResource("mchc_ir_005.xml"); + assertNotNull(itemUrl); + File itemFile = new File(itemUrl.toURI()); + + ItemFileResourceValidator validator = new ItemFileResourceValidator(); + boolean valid = validator.validate(itemFile.getName(), itemFile); + Assert.assertTrue(valid); + } + + + @Test + public void testItemValidation_zip() throws IOException, URISyntaxException { + URL itemUrl = FileResourceValidatorTest.class.getResource("mchc_i_002.zip"); + assertNotNull(itemUrl); + File itemFile = new File(itemUrl.toURI()); + + ItemFileResourceValidator validator = new ItemFileResourceValidator(); + boolean valid = validator.validate(itemFile.getName(), itemFile); + Assert.assertTrue(valid); + } + +} diff --git a/src/test/java/org/olat/modules/ims/qti/fileresource/mchc_i_002.zip b/src/test/java/org/olat/modules/ims/qti/fileresource/mchc_i_002.zip new file mode 100644 index 0000000000000000000000000000000000000000..48e31fd9ff28948e18f0bcd256acbbaa318f8533 GIT binary patch literal 13846 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�cFq@y4Q<txrteE*$ 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%0D; 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 literal 0 HcmV?d00001 diff --git a/src/test/java/org/olat/modules/ims/qti/fileresource/mchc_ir_005.xml b/src/test/java/org/olat/modules/ims/qti/fileresource/mchc_ir_005.xml new file mode 100644 index 00000000000..b285c730163 --- /dev/null +++ b/src/test/java/org/olat/modules/ims/qti/fileresource/mchc_ir_005.xml @@ -0,0 +1 @@ +<?xml version = "1.0" encoding = "UTF-8" standalone = "no"?> <!DOCTYPE questestinterop SYSTEM "ims_qtiasiv1p2.dtd"> <!-- Author: Colin Smythe --> <!-- Date: 22nd January, 2002 --> <!-- Version 1.2 Compliant Example: BasicExample008b --> <!-- Basic Example with response processing --> <questestinterop> <qticomment>This is a multiple-choice with slider rendering.</qticomment> <item title = "Multiple Choice with Slider rendering Item" ident = "IMS_V01_I_mchc_ir_005"> <presentation label = "BasicExample008b"> <flow> <material> <mattext>What is the value of 2 * 3 ?</mattext> </material> <flow> <response_lid ident = "MC05" rcardinality = "Single" rtiming = "No"> <render_slider lowerbound = "2" upperbound = "10" step = "2" startval = "4" steplabel = "Yes"> <response_label ident = "A" rrange = "Exact">2</response_label> <response_label ident = "B" rrange = "Exact">4</response_label> <response_label ident = "C" rrange = "Exact">6</response_label> <response_label ident = "D" rrange = "Exact">8</response_label> <response_label ident = "E" rrange = "Exact">10</response_label> </render_slider> </response_lid> </flow> </flow> </presentation> <resprocessing> <outcomes> <decvar varname = "SLIDECHOICE" vartype = "Integer" defaultval = "0"/> </outcomes> <respcondition> <qticomment>Scoring for the correct answer.</qticomment> <conditionvar> <varequal respident = "MC05">C</varequal> </conditionvar> <setvar action = "Add" varname = "SLIDECHOICE">5</setvar> <displayfeedback feedbacktype = "Response" linkrefid = "Correct"/> </respcondition> <respcondition> <qticomment>Detecting the worng answer.</qticomment> <conditionvar> <or> <varequal respident = "MC05">A</varequal> <varequal respident = "MC05">B</varequal> <varequal respident = "MC05">D</varequal> <varequal respident = "MC05">E</varequal> </or> </conditionvar> <displayfeedback feedbacktype = "Response" linkrefid = "Incorrect"/> </respcondition> </resprocessing> <itemfeedback ident = "Correct" view = "Candidate"> <flow_mat> <material> <mattext>Correct.</mattext> </material> </flow_mat> </itemfeedback> <itemfeedback ident = "Incorrect" view = "Candidate"> <flow_mat> <material> <mattext>The correct answer is 6.</mattext> </material> </flow_mat> </itemfeedback> <itemfeedback ident = "Incorrect" view = "Tutor"> <flow_mat> <material> <mattext>The student chose the wrong answer.</mattext> </material> </flow_mat> </itemfeedback> </item> </questestinterop> \ No newline at end of file diff --git a/src/test/java/org/olat/modules/qpool/manager/CollectionDAOTest.java b/src/test/java/org/olat/modules/qpool/manager/CollectionDAOTest.java index 6e1f7440f42..a10b1336d4a 100644 --- a/src/test/java/org/olat/modules/qpool/manager/CollectionDAOTest.java +++ b/src/test/java/org/olat/modules/qpool/manager/CollectionDAOTest.java @@ -79,7 +79,7 @@ public class CollectionDAOTest extends OlatTestCase { public void addItemToCollectionById() { Identity id = JunitTestHelper.createAndPersistIdentityAsUser("Coll-Onwer-2-" + UUID.randomUUID().toString()); QuestionItemCollection coll = collectionDao.createCollection("NGC collection 2", id); - QuestionItem item = questionDao.create(null, "NGC 89", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, QuestionType.FIB); + QuestionItem item = questionDao.create(null, "NGC 89", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, null, QuestionType.FIB); dbInstance.commitAndCloseSession(); //add the item to the collection @@ -92,8 +92,8 @@ public class CollectionDAOTest extends OlatTestCase { //create a collection with 2 items Identity id = JunitTestHelper.createAndPersistIdentityAsUser("Coll-Onwer-3-" + UUID.randomUUID().toString()); QuestionItemCollection coll = collectionDao.createCollection("NGC collection 3", id); - QuestionItem item1 = questionDao.create(null, "NGC 92", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, QuestionType.FIB); - QuestionItem item2 = questionDao.create(null, "NGC 97", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, QuestionType.FIB); + QuestionItem item1 = questionDao.create(null, "NGC 92", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, null, QuestionType.FIB); + QuestionItem item2 = questionDao.create(null, "NGC 97", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, null, QuestionType.FIB); collectionDao.addItemToCollection(item1, coll); collectionDao.addItemToCollection(item2, coll); dbInstance.commit();//check if it's alright diff --git a/src/test/java/org/olat/modules/qpool/manager/PoolDAOTest.java b/src/test/java/org/olat/modules/qpool/manager/PoolDAOTest.java index 22fc57739fb..72f243c78ab 100644 --- a/src/test/java/org/olat/modules/qpool/manager/PoolDAOTest.java +++ b/src/test/java/org/olat/modules/qpool/manager/PoolDAOTest.java @@ -92,7 +92,7 @@ public class PoolDAOTest extends OlatTestCase { String name = "NGC-" + UUID.randomUUID().toString(); Pool pool = poolDao.createPool(name); Assert.assertNotNull(pool); - QuestionItem item = questionItemDao.create(null, "Galaxy", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item = questionItemDao.create(null, "Galaxy", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); Assert.assertNotNull(item); dbInstance.commitAndCloseSession(); @@ -106,7 +106,7 @@ public class PoolDAOTest extends OlatTestCase { //create a pool String name = "NGC-" + UUID.randomUUID().toString(); Pool pool = poolDao.createPool(name); - QuestionItem item = questionItemDao.create(null, "Galaxy", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item = questionItemDao.create(null, "Galaxy", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); poolDao.addItemToPool(item, pool); dbInstance.commitAndCloseSession(); @@ -121,7 +121,7 @@ public class PoolDAOTest extends OlatTestCase { //create a pool with an item String name = "NGC-" + UUID.randomUUID().toString(); Pool pool = poolDao.createPool(name); - QuestionItem item = questionItemDao.create(null, "Galaxy", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item = questionItemDao.create(null, "Galaxy", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); poolDao.addItemToPool(item, pool); dbInstance.commitAndCloseSession(); diff --git a/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java b/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java index 3a90b5090a3..5efa4c10288 100644 --- a/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java +++ b/src/test/java/org/olat/modules/qpool/manager/QuestionDAOTest.java @@ -57,9 +57,10 @@ public class QuestionDAOTest extends OlatTestCase { @Test public void createQuestion() { - QuestionItem item = questionDao.create(null, "Stars", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.FIB); + QuestionItem item = questionDao.create(null, "Stars", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.FIB); Assert.assertNotNull(item); Assert.assertNotNull(item.getKey()); + Assert.assertNotNull(item.getUuid()); Assert.assertNotNull(item.getCreationDate()); Assert.assertNotNull(item.getLastModified()); Assert.assertNotNull(item.getQuestionType()); @@ -71,7 +72,7 @@ public class QuestionDAOTest extends OlatTestCase { @Test public void createQuestion_withOwner() { Identity id = JunitTestHelper.createAndPersistIdentityAsUser("QOwn-1-" + UUID.randomUUID().toString()); - QuestionItem item = questionDao.create(id, "My fav. stars", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.FIB); + QuestionItem item = questionDao.create(id, "My fav. stars", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.FIB); Assert.assertNotNull(item); Assert.assertNotNull(item.getKey()); Assert.assertNotNull(item.getCreationDate()); @@ -86,8 +87,8 @@ public class QuestionDAOTest extends OlatTestCase { public void getItems_byAuthor() { //create an author with 2 items Identity id = JunitTestHelper.createAndPersistIdentityAsUser("QOwn-2-" + UUID.randomUUID().toString()); - QuestionItem item1 = questionDao.create(id, "NGC 2171", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.FIB); - QuestionItem item2 = questionDao.create(id, "NGC 2172", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.FIB); + QuestionItem item1 = questionDao.create(id, "NGC 2171", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.FIB); + QuestionItem item2 = questionDao.create(id, "NGC 2172", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.FIB); dbInstance.commitAndCloseSession(); //count the items of the author @@ -103,7 +104,7 @@ public class QuestionDAOTest extends OlatTestCase { @Test public void getNumOfQuestions() { - QuestionItem item = questionDao.create(null, "NGC 1277", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item = questionDao.create(null, "NGC 1277", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); Assert.assertNotNull(item); dbInstance.commitAndCloseSession(); @@ -115,9 +116,9 @@ public class QuestionDAOTest extends OlatTestCase { @Test public void getFavoritItems() { Identity id = JunitTestHelper.createAndPersistIdentityAsUser("fav-item-" + UUID.randomUUID().toString()); - QuestionItem item1 = questionDao.create(id, "NGC 55", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); - QuestionItem item2 = questionDao.create(id, "NGC 253", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); - QuestionItem item3 = questionDao.create(id, "NGC 292", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item1 = questionDao.create(id, "NGC 55", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); + QuestionItem item2 = questionDao.create(id, "NGC 253", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); + QuestionItem item3 = questionDao.create(id, "NGC 292", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); markManager.setMark(item1, id, null, "[QuestionItem:" + item1 + "]"); markManager.setMark(item2, id, null, "[QuestionItem:" + item2 + "]"); dbInstance.commitAndCloseSession(); @@ -134,8 +135,8 @@ public class QuestionDAOTest extends OlatTestCase { public void shareItems() { //create a group to share 2 items BusinessGroup group = businessGroupDao.createAndPersist(null, "gdao", "gdao-desc", -1, -1, false, false, false, false, false); - QuestionItem item1 = questionDao.create(null, "Share-Item-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); - QuestionItem item2 = questionDao.create(null, "Share-Item-2", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item1 = questionDao.create(null, "Share-Item-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); + QuestionItem item2 = questionDao.create(null, "Share-Item-2", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); dbInstance.commit(); //share them @@ -154,7 +155,7 @@ public class QuestionDAOTest extends OlatTestCase { public void shareItems_avoidDuplicates() { //create a group to share 2 items BusinessGroup group = businessGroupDao.createAndPersist(null, "gdao", "gdao-desc", -1, -1, false, false, false, false, false); - QuestionItem item = questionDao.create(null, "Share-Item-Dup-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item = questionDao.create(null, "Share-Item-Dup-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); dbInstance.commit(); //share them @@ -174,7 +175,7 @@ public class QuestionDAOTest extends OlatTestCase { //create a group to share 2 items Identity id = JunitTestHelper.createAndPersistIdentityAsUser("Share-item-" + UUID.randomUUID().toString()); BusinessGroup group = businessGroupDao.createAndPersist(id, "gdao", "gdao-desc", -1, -1, false, false, false, false, false); - QuestionItem item = questionDao.create(id, "Share-Item-Dup-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item = questionDao.create(id, "Share-Item-Dup-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); dbInstance.commit(); //share them @@ -194,7 +195,7 @@ public class QuestionDAOTest extends OlatTestCase { //create a group to share 2 items Identity id = JunitTestHelper.createAndPersistIdentityAsUser("Share-rm-" + UUID.randomUUID().toString()); BusinessGroup group = businessGroupDao.createAndPersist(id, "gdrm", "gdrm-desc", -1, -1, false, false, false, false, false); - QuestionItem item = questionDao.create(id, "Share-item-rm-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item = questionDao.create(id, "Share-item-rm-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); dbInstance.commit(); //share them questionDao.share(item, group.getResource()); diff --git a/src/test/java/org/olat/modules/qpool/manager/QuestionPoolServiceTest.java b/src/test/java/org/olat/modules/qpool/manager/QuestionPoolServiceTest.java index bd1876ddb92..4028907f9eb 100644 --- a/src/test/java/org/olat/modules/qpool/manager/QuestionPoolServiceTest.java +++ b/src/test/java/org/olat/modules/qpool/manager/QuestionPoolServiceTest.java @@ -19,6 +19,12 @@ */ package org.olat.modules.qpool.manager; +import static org.junit.Assert.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -62,8 +68,8 @@ public class QuestionPoolServiceTest extends OlatTestCase { //create a group to share 2 items Identity id = JunitTestHelper.createAndPersistIdentityAsUser("Share-rm-" + UUID.randomUUID().toString()); BusinessGroup group = businessGroupDao.createAndPersist(id, "gdrm", "gdrm-desc", -1, -1, false, false, false, false, false); - QuestionItem item1 = questionDao.create(id, "Share-item-rm-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); - QuestionItem item2 = questionDao.create(id, "Share-item-rm-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, QuestionType.MC); + QuestionItem item1 = questionDao.create(id, "Share-item-rm-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); + QuestionItem item2 = questionDao.create(id, "Share-item-rm-1", QTIConstants.QTI_12_FORMAT, Locale.ENGLISH.getLanguage(), null, null, QuestionType.MC); dbInstance.commit(); //share them questionDao.share(item1, group.getResource()); @@ -86,9 +92,9 @@ public class QuestionPoolServiceTest extends OlatTestCase { @Test public void createCollection() { //create an user with 2 items - Identity id = JunitTestHelper.createAndPersistIdentityAsUser("Coll-Onwer-3-" + UUID.randomUUID().toString()); - QuestionItem item1 = questionDao.create(id, "NGC 92", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, QuestionType.FIB); - QuestionItem item2 = questionDao.create(id, "NGC 97", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, QuestionType.FIB); + Identity id = JunitTestHelper.createAndPersistIdentityAsUser("Coll-Owner-3-" + UUID.randomUUID().toString()); + QuestionItem item1 = questionDao.create(id, "NGC 92", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, null, QuestionType.FIB); + QuestionItem item2 = questionDao.create(id, "NGC 97", QTIConstants.QTI_12_FORMAT, Locale.GERMAN.getLanguage(), null, null, QuestionType.FIB); dbInstance.commit(); //load the items of the collection @@ -109,4 +115,19 @@ public class QuestionPoolServiceTest extends OlatTestCase { Assert.assertTrue(itemsOfCollection.contains(item1)); Assert.assertTrue(itemsOfCollection.contains(item2)); } + + @Test + public void importItem_qti12xml() throws IOException, URISyntaxException { + Identity owner = JunitTestHelper.createAndPersistIdentityAsUser("Imp-Owner-1-" + UUID.randomUUID().toString()); + dbInstance.commit(); + URL itemUrl = QuestionPoolServiceTest.class.getResource("mchc_i_001.xml"); + assertNotNull(itemUrl); + File itemFile = new File(itemUrl.toURI()); + + + qpoolService.importItem(owner, "mchc_i_001.xml", itemFile); + + + } + } diff --git a/src/test/java/org/olat/modules/qpool/manager/mchc_i_001.xml b/src/test/java/org/olat/modules/qpool/manager/mchc_i_001.xml new file mode 100644 index 00000000000..e865fa086bd --- /dev/null +++ b/src/test/java/org/olat/modules/qpool/manager/mchc_i_001.xml @@ -0,0 +1 @@ +<?xml version = "1.0" encoding = "UTF-8" standalone = "no"?> <!-- Author: Colin Smythe --> <!-- Date: 22nd January --> <!-- Version 1.2 Compliant Example: BasicExample002a --> <questestinterop> <qticomment>This is a simple multiple choice example. The rendering is a standard radio button style. No response processing is incorporated.</qticomment> <item title = "Standard Multiple Choice Item" ident = "IMS_V01_I_mchc_i_001"> <presentation label = "BasicExample002a"> <flow> <material> <mattext>Which one of the listed standards committees is responsible for developing the token ring specification ?</mattext> </material> <response_lid ident = "MCa_01" rcardinality = "Single" rtiming = "No"> <render_choice shuffle = "Yes"> <flow_label> <response_label ident = "A"> <material> <mattext>IEEE 802.3</mattext> </material> </response_label> </flow_label> <flow_label> <response_label ident = "B"> <material> <mattext>IEEE 802.5</mattext> </material> </response_label> </flow_label> <flow_label> <response_label ident = "C"> <material> <mattext>IEEE 802.6</mattext> </material> </response_label> </flow_label> <flow_label> <response_label ident = "D"> <material> <mattext>IEEE 802.11</mattext> </material> </response_label> </flow_label> <flow_label> <response_label ident = "E" rshuffle = "No"> <material> <mattext>None of the above.</mattext> </material> </response_label> </flow_label> </render_choice> </response_lid> </flow> </presentation> </item> </questestinterop> \ No newline at end of file diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java index 5e4647c8216..465854c0355 100644 --- a/src/test/java/org/olat/test/AllTestsJunit4.java +++ b/src/test/java/org/olat/test/AllTestsJunit4.java @@ -117,6 +117,7 @@ import org.junit.runners.Suite; org.olat.course.nodes.projectbroker.ProjectBrokerManagerTest.class, org.olat.core.commons.persistence.DBTest.class, org.olat.modules.ims.cp.CPManagerTest.class, + org.olat.modules.ims.qti.fileresource.FileResourceValidatorTest.class, org.olat.modules.webFeed.FeedManagerImplTest.class, org.olat.modules.qpool.manager.QuestionDAOTest.class, org.olat.modules.qpool.manager.CollectionDAOTest.class, -- GitLab