From 3610c04466010c8473e75dc46f8c93b5bb35f959 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Mon, 15 Feb 2016 18:40:30 +0100
Subject: [PATCH] OO-1593: add preview and editor for QTI 2.1 questions

---
 .../ims/qti21/pool/QTI21EditorController.java | 45 ++++++++++++-
 .../ims/qti21/pool/QTI21ImportProcessor.java  |  2 -
 .../qti21/pool/QTI21PreviewController.java    |  3 +-
 .../qti21/pool/QTI21QPoolServiceProvider.java | 13 +++-
 .../ims/qti21/pool/_content/pool_editor.html  |  1 +
 .../ims/qti21/pool/_content/qti_preview.html  |  1 +
 .../ui/AssessmentItemDisplayController.java   | 66 +++++++++++++++----
 .../AssessmentItemEditorController.java       | 23 ++++++-
 .../SingleChoiceEditorController.java         |  6 ++
 9 files changed, 141 insertions(+), 19 deletions(-)
 create mode 100644 src/main/java/org/olat/ims/qti21/pool/_content/pool_editor.html

diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21EditorController.java b/src/main/java/org/olat/ims/qti21/pool/QTI21EditorController.java
index 8e8126d6bf9..db32dc6dd56 100644
--- a/src/main/java/org/olat/ims/qti21/pool/QTI21EditorController.java
+++ b/src/main/java/org/olat/ims/qti21/pool/QTI21EditorController.java
@@ -19,14 +19,26 @@
  */
 package org.olat.ims.qti21.pool;
 
+import java.io.File;
+import java.net.URI;
+
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.velocity.VelocityContainer;
+import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
 import org.olat.core.gui.control.controller.BasicController;
+import org.olat.ims.qti21.QTI21Service;
+import org.olat.ims.qti21.ui.editor.AssessmentItemEditorController;
+import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent;
 import org.olat.modules.qpool.QPoolItemEditorController;
+import org.olat.modules.qpool.QPoolService;
 import org.olat.modules.qpool.QuestionItem;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
 
 /**
  * 
@@ -37,13 +49,33 @@ import org.olat.modules.qpool.QuestionItem;
 public class QTI21EditorController extends BasicController implements QPoolItemEditorController {
 	
 	private final VelocityContainer mainVC;
+	private AssessmentItemEditorController editorCtrl;
 	
+	private File resourceFile;
 	private QuestionItem questionItem;
 	
+	@Autowired
+	private QPoolService qpoolService;
+	@Autowired
+	private QTI21Service qtiService;
+	
 	public QTI21EditorController(UserRequest ureq, WindowControl wControl, QuestionItem questionItem) {
 		super(ureq, wControl);
 		this.questionItem = questionItem;
-		mainVC = createVelocityContainer("editor_wrapper");
+		mainVC = createVelocityContainer("pool_editor");
+		
+		File resourceDirectory = qpoolService.getRootDirectory(questionItem);
+		resourceFile = qpoolService.getRootFile(questionItem);
+		URI assessmentItemUri = resourceFile.toURI();
+		
+		ResolvedAssessmentItem resolvedAssessmentItem = qtiService
+				.loadAndResolveAssessmentItem(assessmentItemUri, resourceDirectory);
+		
+		editorCtrl = new AssessmentItemEditorController(ureq, wControl,
+				resolvedAssessmentItem, resourceDirectory, resourceFile);
+		listenTo(editorCtrl);
+		mainVC.put("editor", editorCtrl.getInitialComponent());
+		
 		putInitialPanel(mainVC);
 	}
 	
@@ -56,6 +88,17 @@ public class QTI21EditorController extends BasicController implements QPoolItemE
 	protected void doDispose() {
 		//
 	}
+	
+	@Override
+	protected void event(UserRequest ureq, Controller source, Event event) {
+		if(source == editorCtrl) {
+			if(event instanceof AssessmentItemEvent) {
+				AssessmentItemEvent aie = (AssessmentItemEvent)event;
+				AssessmentItem assessmentItem = aie.getAssessmentItem();
+				qtiService.persistAssessmentObject(resourceFile, assessmentItem);
+			}
+		}
+	}
 
 	@Override
 	protected void event(UserRequest ureq, Component source, Event event) {
diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
index c5297fc16a7..3a454dda847 100644
--- a/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
+++ b/src/main/java/org/olat/ims/qti21/pool/QTI21ImportProcessor.java
@@ -120,6 +120,4 @@ public class QTI21ImportProcessor {
 		questionItemDao.persist(owner, poolItem);
 		return poolItem;
 	}
-	
-
 }
diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21PreviewController.java b/src/main/java/org/olat/ims/qti21/pool/QTI21PreviewController.java
index 99c4812b0ba..38d786ed00e 100644
--- a/src/main/java/org/olat/ims/qti21/pool/QTI21PreviewController.java
+++ b/src/main/java/org/olat/ims/qti21/pool/QTI21PreviewController.java
@@ -63,10 +63,11 @@ public class QTI21PreviewController extends BasicController {
 		} else {
 			File resourceDirectory = qpoolService.getRootDirectory(qitem);
 			URI assessmentItemUri = file.toURI();
+			File itemFile = qpoolService.getRootFile(qitem);
 			
 			ResolvedAssessmentItem resolvedAssessmentItem = qtiService
 					.loadAndResolveAssessmentItem(assessmentItemUri, resourceDirectory);
-			previewCtrl = new AssessmentItemDisplayController(ureq, wControl, true, resolvedAssessmentItem, resourceDirectory);
+			previewCtrl = new AssessmentItemDisplayController(ureq, wControl, resolvedAssessmentItem, resourceDirectory, itemFile);
 			listenTo(previewCtrl);
 			mainVC.put("preview", previewCtrl.getInitialComponent());
 		}
diff --git a/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java b/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java
index 9412f5971c8..39197ca44ab 100644
--- a/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java
+++ b/src/main/java/org/olat/ims/qti21/pool/QTI21QPoolServiceProvider.java
@@ -46,12 +46,14 @@ import org.olat.ims.qti.fileresource.TestFileResource;
 import org.olat.ims.qti21.QTI21Constants;
 import org.olat.ims.qti21.QTI21Service;
 import org.olat.ims.qti21.model.xml.AssessmentItemBuilder;
+import org.olat.ims.qti21.model.xml.ManifestPackage;
 import org.olat.ims.qti21.model.xml.interactions.EssayAssessmentItemBuilder;
 import org.olat.ims.qti21.model.xml.interactions.KPrimAssessmentItemBuilder;
 import org.olat.ims.qti21.model.xml.interactions.MultipleChoiceAssessmentItemBuilder;
 import org.olat.ims.qti21.model.xml.interactions.SingleChoiceAssessmentItemBuilder;
 import org.olat.ims.qti21.pool.QTI21AssessmentItemFactory.Type;
 import org.olat.ims.resources.IMSEntityResolver;
+import org.olat.imscp.xml.manifest.ManifestType;
 import org.olat.modules.qpool.ExportFormatOptions;
 import org.olat.modules.qpool.ExportFormatOptions.Outcome;
 import org.olat.modules.qpool.QItemFactory;
@@ -220,8 +222,9 @@ public class QTI21QPoolServiceProvider implements QPoolSPI {
 	}
 
 	@Override
-	public Controller getEditableController(UserRequest ureq, WindowControl wControl, QuestionItem item) {
-		return null;
+	public Controller getEditableController(UserRequest ureq, WindowControl wControl, QuestionItem qitem) {
+		Controller editorCtrl = new QTI21EditorController(ureq, wControl, qitem);
+		return editorCtrl;
 	}
 
 	public QuestionItem createItem(Identity identity, Type type, String title, Locale locale) {
@@ -247,6 +250,12 @@ public class QTI21QPoolServiceProvider implements QPoolSPI {
 		VFSLeaf leaf = baseDir.createChildLeaf(qitem.getRootFilename());
 		File itemFile = ((LocalImpl)leaf).getBasefile();
 		qtiService.persistAssessmentObject(itemFile, assessmentItem);
+		
+		//create imsmanifest
+		ManifestType manifestType = ManifestPackage.createEmptyManifest();
+        ManifestPackage.appendAssessmentItem(itemFile.getName(), manifestType);	
+        ManifestPackage.write(manifestType, new File(itemFile.getParentFile(), "imsmanifest.xml"));
+		
 		return qitem;
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/pool/_content/pool_editor.html b/src/main/java/org/olat/ims/qti21/pool/_content/pool_editor.html
new file mode 100644
index 00000000000..d9eec727b50
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/pool/_content/pool_editor.html
@@ -0,0 +1 @@
+$r.render("editor")
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/pool/_content/qti_preview.html b/src/main/java/org/olat/ims/qti21/pool/_content/qti_preview.html
index e69de29bb2d..a52735a0088 100644
--- a/src/main/java/org/olat/ims/qti21/pool/_content/qti_preview.html
+++ b/src/main/java/org/olat/ims/qti21/pool/_content/qti_preview.html
@@ -0,0 +1 @@
+$r.render("preview")
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java
index c2b47023b23..e8d3836e62f 100644
--- a/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentItemDisplayController.java
@@ -86,7 +86,7 @@ public class AssessmentItemDisplayController extends BasicController implements
 	
 	private final String mapperUri;
 	private final File fUnzippedDirRoot;
-	private final AssessmentItemRef itemRef;
+	private final File itemFileRef;
 	private final ResolvedAssessmentItem resolvedAssessmentItem;
 	
 	private CandidateEvent lastEvent;
@@ -96,28 +96,70 @@ public class AssessmentItemDisplayController extends BasicController implements
 	@Autowired
 	private QTI21Service qtiService;
 	
-	public AssessmentItemDisplayController(UserRequest ureq, WindowControl wControl,
-			boolean authorMode, ResolvedAssessmentItem resolvedAssessmentItem, File fUnzippedDirRoot) {
-		this(ureq, wControl, null, null, authorMode, false, resolvedAssessmentItem, null, fUnzippedDirRoot);
+	/**
+	 * OPen in memory session
+	 * @param ureq
+	 * @param wControl
+	 * @param authorMode
+	 * @param resolvedAssessmentItem
+	 * @param fUnzippedDirRoot
+	 * @param itemFileRef
+	 */
+	public AssessmentItemDisplayController(UserRequest ureq, WindowControl wControl, ResolvedAssessmentItem resolvedAssessmentItem,
+			File fUnzippedDirRoot, File itemFileRef) {
+		super(ureq, wControl);
+		
+		this.itemFileRef = itemFileRef;
+		this.fUnzippedDirRoot = fUnzippedDirRoot;
+		this.resolvedAssessmentItem = resolvedAssessmentItem;
+		currentRequestTimestamp = ureq.getRequestTimestamp();
+		candidateSession = new InMemoryAssessmentTestSession();
+		mapperUri = registerCacheableMapper(null, UUID.randomUUID().toString(), new ResourcesMapper(itemFileRef.toURI()));
+		
+		itemSessionController = enterSession(ureq);
+		
+		if (itemSessionController.getItemSessionState().isEnded()) {
+			mainVC = createVelocityContainer("end");
+		} else {
+			mainVC = createVelocityContainer("run");
+        	initQtiWorks(ureq);
+		}
+		putInitialPanel(mainVC);
 	}
 	
 	public AssessmentItemDisplayController(UserRequest ureq, WindowControl wControl,
-			RepositoryEntry testEntry, AssessmentEntry assessmentEntry, boolean authorMode, boolean persistent,
 			ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, File fUnzippedDirRoot) {
 		super(ureq, wControl);
 		
-		this.itemRef = itemRef;
+		this.itemFileRef = new File(fUnzippedDirRoot, itemRef.getHref().toString());
 		this.fUnzippedDirRoot = fUnzippedDirRoot;
 		this.resolvedAssessmentItem = resolvedAssessmentItem;
 		currentRequestTimestamp = ureq.getRequestTimestamp();
-		if(persistent) {
-			candidateSession = qtiService.createAssessmentTestSession(getIdentity(), assessmentEntry, testEntry, itemRef.getIdentifier().toString(), testEntry, authorMode);
+		candidateSession = new InMemoryAssessmentTestSession();
+		mapperUri = registerCacheableMapper(null, UUID.randomUUID().toString(), new ResourcesMapper(itemFileRef.toURI()));
+		
+		itemSessionController = enterSession(ureq);
+		
+		if (itemSessionController.getItemSessionState().isEnded()) {
+			mainVC = createVelocityContainer("end");
 		} else {
-			candidateSession = new InMemoryAssessmentTestSession();
+			mainVC = createVelocityContainer("run");
+        	initQtiWorks(ureq);
 		}
+		putInitialPanel(mainVC);
+	}
+	
+	public AssessmentItemDisplayController(UserRequest ureq, WindowControl wControl,
+			RepositoryEntry testEntry, AssessmentEntry assessmentEntry, boolean authorMode,
+			ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, File fUnzippedDirRoot) {
+		super(ureq, wControl);
 		
-		File assessmentObjectUri = itemRef == null ? null : new File(fUnzippedDirRoot, itemRef.getHref().toString());
-		mapperUri = registerCacheableMapper(null, UUID.randomUUID().toString(), new ResourcesMapper(assessmentObjectUri.toURI()));
+		this.itemFileRef = new File(fUnzippedDirRoot, itemRef.getHref().toString());
+		this.fUnzippedDirRoot = fUnzippedDirRoot;
+		this.resolvedAssessmentItem = resolvedAssessmentItem;
+		currentRequestTimestamp = ureq.getRequestTimestamp();
+		candidateSession = qtiService.createAssessmentTestSession(getIdentity(), assessmentEntry, testEntry, itemRef.getIdentifier().toString(), testEntry, authorMode);
+		mapperUri = registerCacheableMapper(null, UUID.randomUUID().toString(), new ResourcesMapper(itemFileRef.toURI()));
 		
 		itemSessionController = enterSession(ureq);
 		
@@ -131,7 +173,7 @@ public class AssessmentItemDisplayController extends BasicController implements
 	}
 	
 	private void initQtiWorks(UserRequest ureq) {
-		String filename = itemRef.getHref().toString();
+		String filename = itemFileRef.getName();
 		qtiWorksCtrl = new QtiWorksController(ureq, getWindowControl(), filename);
     	listenTo(qtiWorksCtrl);
     	mainVC.put("qtirun", qtiWorksCtrl.getInitialComponent());
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
index 519d916d2da..c815989b441 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
@@ -83,6 +83,27 @@ public class AssessmentItemEditorController extends BasicController {
 	@Autowired
 	private AssessmentService assessmentService;
 	
+	
+	public AssessmentItemEditorController(UserRequest ureq, WindowControl wControl,
+			ResolvedAssessmentItem resolvedAssessmentItem, File unzippedDirectory, File itemFile) {
+		super(ureq, wControl);
+		this.itemRef = null;
+		this.resolvedAssessmentItem = resolvedAssessmentItem;
+		
+		mainVC = createVelocityContainer("assessment_item_editor");
+		tabbedPane = new TabbedPane("itemTabs", getLocale());
+		tabbedPane.addListener(this);
+		mainVC.put("tabbedpane", tabbedPane);
+
+		initItemEditor(ureq);
+		
+		displayCtrl = new AssessmentItemDisplayController(ureq, getWindowControl(), resolvedAssessmentItem, unzippedDirectory, itemFile);
+		listenTo(displayCtrl);
+		tabbedPane.addTab("Preview", displayCtrl.getInitialComponent());
+		
+		putInitialPanel(mainVC);
+	}
+	
 	public AssessmentItemEditorController(UserRequest ureq, WindowControl wControl, RepositoryEntry testEntry,
 			ResolvedAssessmentItem resolvedAssessmentItem, AssessmentItemRef itemRef, File unzippedDirectory) {
 		super(ureq, wControl);
@@ -98,7 +119,7 @@ public class AssessmentItemEditorController extends BasicController {
 		
 		AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(getIdentity(), testEntry, null, testEntry);
 		displayCtrl = new AssessmentItemDisplayController(ureq, getWindowControl(),
-				testEntry, assessmentEntry, true, true, resolvedAssessmentItem, itemRef, unzippedDirectory);
+				testEntry, assessmentEntry, true, resolvedAssessmentItem, itemRef, unzippedDirectory);
 		listenTo(displayCtrl);
 		tabbedPane.addTab("Preview", displayCtrl.getInitialComponent());
 		
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java
index 414ad7657ff..cd29a5b65c9 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/SingleChoiceEditorController.java
@@ -175,6 +175,12 @@ public class SingleChoiceEditorController extends FormBasicController {
 			allOk &= false;
 		}
 		
+		String correctAnswer = ureq.getParameter("correct");
+		if(!StringHelper.containsNonWhitespace(correctAnswer)) {
+			allOk &= false;
+			textEl.setErrorKey("form.legende.mandatory", null);
+		}
+		
 		return allOk & super.validateFormLogic(ureq);
 	}
 
-- 
GitLab