From 71e7a4430abc87579c799208b46105f4ecd1d54e Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Tue, 13 Nov 2012 08:36:39 +0100
Subject: [PATCH] OO-417: refactor the submit process in QTI, all the back end
 jobs are done at the same time with delegates, course node attempts
 increments, save on the database, generating reports...

---
 .../course/nodes/iq/IQEditController.java     |  2 +-
 .../course/nodes/iq/IQEditController.java     |  2 +-
 .../olat/course/nodes/iq/IQRunController.java | 74 ++++++++-------
 .../ims/qti/QTIResultDetailsController.java   | 32 ++++---
 .../ims/qti/navigator/DefaultNavigator.java   | 21 ++++-
 .../ims/qti/navigator/MenuItemNavigator.java  |  4 +-
 .../qti/navigator/MenuSectionNavigator.java   |  4 +-
 .../org/olat/ims/qti/navigator/Navigator.java |  1 +
 .../ims/qti/navigator/NavigatorDelegate.java  | 34 +++++++
 .../navigator/SequentialItemNavigator.java    |  4 +-
 .../navigator/SequentialSectionNavigator.java |  4 +-
 .../ims/qti/process/AssessmentFactory.java    | 11 ++-
 .../ims/qti/process/AssessmentInstance.java   | 21 +++--
 .../olat/modules/iq/IQDisplayController.java  | 90 ++++++++++---------
 .../java/org/olat/modules/iq/IQManager.java   |  5 +-
 .../org/olat/modules/iq/IQSubmittedEvent.java | 38 +-------
 .../course/CourseAssessmentWebService.java    |  2 +-
 17 files changed, 199 insertions(+), 150 deletions(-)
 create mode 100644 src/main/java/org/olat/ims/qti/navigator/NavigatorDelegate.java

diff --git a/src/main/java/de/bps/onyx/plugin/course/nodes/iq/IQEditController.java b/src/main/java/de/bps/onyx/plugin/course/nodes/iq/IQEditController.java
index df8ed672182..b4465b95854 100644
--- a/src/main/java/de/bps/onyx/plugin/course/nodes/iq/IQEditController.java
+++ b/src/main/java/de/bps/onyx/plugin/course/nodes/iq/IQEditController.java
@@ -445,7 +445,7 @@ public class IQEditController extends ActivateableTabbableDefaultController impl
 			// handle preview
 			if (previewLayoutCtr != null) previewLayoutCtr.dispose();
 			Controller previewController = IQManager.getInstance().createIQDisplayController(moduleConfiguration, new IQPreviewSecurityCallback(), ureq, getWindowControl(), course
-					.getResourceableId().longValue(), courseNode.getIdent());
+					.getResourceableId().longValue(), courseNode.getIdent(), null);
 			previewLayoutCtr = new LayoutMain3ColsPreviewController(ureq, getWindowControl(), null, null, previewController.getInitialComponent(), null);
 			previewLayoutCtr.addDisposableChildController(previewController);
 			previewLayoutCtr.activate();
diff --git a/src/main/java/org/olat/course/nodes/iq/IQEditController.java b/src/main/java/org/olat/course/nodes/iq/IQEditController.java
index c5bb084bdce..424f9d60bfc 100644
--- a/src/main/java/org/olat/course/nodes/iq/IQEditController.java
+++ b/src/main/java/org/olat/course/nodes/iq/IQEditController.java
@@ -386,7 +386,7 @@ public class IQEditController extends ActivateableTabbableDefaultController impl
 		} else if (source == previewLink){
 			// handle preview
 			Controller previewController = IQManager.getInstance().createIQDisplayController(moduleConfiguration, new IQPreviewSecurityCallback(), ureq, getWindowControl(), course
-					.getResourceableId().longValue(), courseNode.getIdent());
+					.getResourceableId().longValue(), courseNode.getIdent(), null);
 			previewLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), previewController);
 			stackPanel.pushController(translate("preview"), previewLayoutCtr);
 			
diff --git a/src/main/java/org/olat/course/nodes/iq/IQRunController.java b/src/main/java/org/olat/course/nodes/iq/IQRunController.java
index 90fb27c0603..34afc919de1 100644
--- a/src/main/java/org/olat/course/nodes/iq/IQRunController.java
+++ b/src/main/java/org/olat/course/nodes/iq/IQRunController.java
@@ -75,6 +75,8 @@ import org.olat.course.nodes.SelfAssessableCourseNode;
 import org.olat.course.run.scoring.ScoreEvaluation;
 import org.olat.course.run.userview.UserCourseEnvironment;
 import org.olat.ims.qti.QTIChangeLogMessage;
+import org.olat.ims.qti.container.AssessmentContext;
+import org.olat.ims.qti.navigator.NavigatorDelegate;
 import org.olat.ims.qti.process.AssessmentInstance;
 import org.olat.ims.qti.process.ImsRepositoryResolver;
 import org.olat.instantMessaging.InstantMessaging;
@@ -95,7 +97,7 @@ import org.olat.util.logging.activity.LoggingResourceable;
  * Initial Date:  Oct 13, 2004
  * @author Felix Jost
  */
-public class IQRunController extends BasicController implements GenericEventListener, Activateable2 {
+public class IQRunController extends BasicController implements GenericEventListener, Activateable2, NavigatorDelegate {
 
 	private VelocityContainer myContent;
 	
@@ -384,7 +386,7 @@ public class IQRunController extends BasicController implements GenericEventList
 			OLATResourceable ores = OresHelper.createOLATResourceableTypeWithoutCheck("test");
 			ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrapBusinessPath(ores));
 			WindowControl bwControl = addToHistory(ureq, ores, null);
-			Controller returnController = IQManager.getInstance().createIQDisplayController(modConfig, secCallback, ureq, bwControl, callingResId, callingResDetail);
+			Controller returnController = IQManager.getInstance().createIQDisplayController(modConfig, secCallback, ureq, bwControl, callingResId, callingResDetail, this);
 			/*
 			 * either returnController is a MessageController or it is a IQDisplayController
 			 * this should not serve as pattern to be copy&pasted.
@@ -454,44 +456,56 @@ public class IQRunController extends BasicController implements GenericEventList
 		}
 	}
 
+	@Override
+	public void submitAssessment(AssessmentInstance ai) {
+		if (type.equals(AssessmentInstance.QMD_ENTRY_TYPE_ASSESS)) {
+			AssessmentContext ac = ai.getAssessmentContext();
+			Float score = new Float(ac.getScore());
+			Boolean passed = new Boolean(ac.isPassed());
+			ScoreEvaluation sceval = new ScoreEvaluation(score, passed, new Long(ai.getAssessID()));
+			AssessableCourseNode acn = (AssessableCourseNode)courseNode; // assessment nodes are assesable			
+			boolean incrementUserAttempts = true;
+			acn.updateUserScoreEvaluation(sceval, userCourseEnv, getIdentity(), incrementUserAttempts);
+				
+			// Mark publisher for notifications
+			AssessmentNotificationsHandler anh = AssessmentNotificationsHandler.getInstance();
+			Long courseId = userCourseEnv.getCourseEnvironment().getCourseResourceableId();
+			anh.markPublisherNews(getIdentity(), courseId);
+			if(!assessmentStopped) {
+			  assessmentStopped = true;					  
+			  AssessmentEvent assessmentStoppedEvent = new AssessmentEvent(AssessmentEvent.TYPE.STOPPED, userSession);
+			  singleUserEventCenter.deregisterFor(this, assessmentInstanceOres);
+				singleUserEventCenter.fireEventToListenersOf(assessmentStoppedEvent, assessmentEventOres);
+			}
+		} else if (type.equals(AssessmentInstance.QMD_ENTRY_TYPE_SURVEY)) {
+			// save number of attempts
+			// although this is not an assessable node we still use the assessment
+			// manager since this one uses caching
+			AssessmentManager am = userCourseEnv.getCourseEnvironment().getAssessmentManager();
+			am.incrementNodeAttempts(courseNode, getIdentity(), userCourseEnv);
+		} else if(type.equals(AssessmentInstance.QMD_ENTRY_TYPE_SELF)){
+			AssessmentManager am = userCourseEnv.getCourseEnvironment().getAssessmentManager();
+			am.incrementNodeAttempts(courseNode, getIdentity(), userCourseEnv);
+		}
+	}
+
+	@Override
+	public void cancelAssessment(AssessmentInstance ai) {
+		//
+	}
+
 	/**
 	 * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
 	 */
 	public void event(UserRequest urequest, Controller source, Event event) {
 		if (source == displayController) {
 			if (event instanceof IQSubmittedEvent) {
-				IQSubmittedEvent se = (IQSubmittedEvent) event;
-				AssessmentManager am = userCourseEnv.getCourseEnvironment().getAssessmentManager();
-				
 				// Save results in case of test
-				if (type.equals(AssessmentInstance.QMD_ENTRY_TYPE_ASSESS)) {
-					// update scoring overview for the user in the current course
-					Float score = new Float(se.getScore());
-					Boolean passed = new Boolean(se.isPassed());
-					ScoreEvaluation sceval = new ScoreEvaluation(score, passed, new Long(se.getAssessmentID()));
-					AssessableCourseNode acn = (AssessableCourseNode)courseNode; // assessment nodes are assesable			
-					boolean incrementUserAttempts = true;
-					acn.updateUserScoreEvaluation(sceval, userCourseEnv, urequest.getIdentity(), incrementUserAttempts);
-					//userCourseEnv.getScoreAccounting().scoreInfoChanged(acn, sceval);					
+				if (type.equals(AssessmentInstance.QMD_ENTRY_TYPE_ASSESS)) {		
 					exposeUserTestDataToVC(urequest);
-										
-					// Mark publisher for notifications
-					AssessmentNotificationsHandler anh = AssessmentNotificationsHandler.getInstance();
-					Long courseId = userCourseEnv.getCourseEnvironment().getCourseResourceableId();
-					anh.markPublisherNews(urequest.getIdentity(), courseId);
-					if(!assessmentStopped) {
-					  assessmentStopped = true;					  
-					  AssessmentEvent assessmentStoppedEvent = new AssessmentEvent(AssessmentEvent.TYPE.STOPPED, userSession);
-					  singleUserEventCenter.deregisterFor(this, assessmentInstanceOres);
-						singleUserEventCenter.fireEventToListenersOf(assessmentStoppedEvent, assessmentEventOres);
-					}
 				} 
 				// Save results in case of questionnaire
 				else if (type.equals(AssessmentInstance.QMD_ENTRY_TYPE_SURVEY)) {
-					// save number of attempts
-					// although this is not an assessable node we still use the assessment
-					// manager since this one uses caching
-					am.incrementNodeAttempts(courseNode, urequest.getIdentity(), userCourseEnv);
 					exposeUserQuestionnaireDataToVC();
 					
 					if(displayContainerController != null) {
@@ -505,7 +519,7 @@ public class IQRunController extends BasicController implements GenericEventList
 				// Don't save results in case of self-test
 				// but do safe attempts !
 				else if(type.equals(AssessmentInstance.QMD_ENTRY_TYPE_SELF)){
-					am.incrementNodeAttempts(courseNode, urequest.getIdentity(), userCourseEnv);
+					//am.incrementNodeAttempts(courseNode, urequest.getIdentity(), userCourseEnv);
 				}
 			} else if (event.equals(Event.DONE_EVENT)) {
 				stopAssessment(urequest, event);
diff --git a/src/main/java/org/olat/ims/qti/QTIResultDetailsController.java b/src/main/java/org/olat/ims/qti/QTIResultDetailsController.java
index c2f45bf0dea..ec36cd7c8ff 100644
--- a/src/main/java/org/olat/ims/qti/QTIResultDetailsController.java
+++ b/src/main/java/org/olat/ims/qti/QTIResultDetailsController.java
@@ -156,19 +156,23 @@ public class QTIResultDetailsController extends BasicController {
 			if (tEvent.getActionId().equals("sel")) {
 				QTIResultSet resultSet = tableModel.getObject(tEvent.getRowId());
 				
-				Document doc = FilePersister.retreiveResultsReporting(assessedIdentity, type, resultSet.getAssessmentID());
-				if (doc == null) {
-					showInfo("error.resreporting.na");
-					return;
-				}
-				StringBuilder resultsHTML = LocalizedXSLTransformer.getInstance(ureq.getLocale()).renderResults(doc);
-				details.contextPut("reshtml", resultsHTML);
-				
-				removeAsListenerAndDispose(cmc);
-				cmc = new CloseableModalController(getWindowControl(), getTranslator().translate("close"), details);
-				listenTo(cmc);
-				
-				cmc.activate();
+				try {
+					Document doc = FilePersister.retreiveResultsReporting(assessedIdentity, type, resultSet.getAssessmentID());
+					if (doc == null) {
+						showInfo("error.resreporting.na");
+						return;
+					}
+					StringBuilder resultsHTML = LocalizedXSLTransformer.getInstance(ureq.getLocale()).renderResults(doc);
+					details.contextPut("reshtml", resultsHTML);
+					
+					removeAsListenerAndDispose(cmc);
+					cmc = new CloseableModalController(getWindowControl(), getTranslator().translate("close"), details);
+					listenTo(cmc);
+					cmc.activate();
+				} catch (Exception e) {
+					logError("", e);
+					showError("error.resreporting.na");
+				}	
 			} else if(tEvent.getActionId().equals("ret")) {
 				updateTableModel();
 				if(tableModel.isTestRunning()) {
@@ -220,7 +224,7 @@ public class QTIResultDetailsController extends BasicController {
 		ModuleConfiguration modConfig = testNode.getModuleConfiguration();
 
 		String resourcePathInfo = courseResourceableId + File.separator + nodeIdent;
-		AssessmentInstance ai = AssessmentFactory.createAssessmentInstance(assessedIdentity, "", modConfig, false, courseResourceableId, nodeIdent, resourcePathInfo);
+		AssessmentInstance ai = AssessmentFactory.createAssessmentInstance(assessedIdentity, "", modConfig, false, courseResourceableId, nodeIdent, resourcePathInfo, null);
 		//close the test
 		ai.close();
 		//persist the results
diff --git a/src/main/java/org/olat/ims/qti/navigator/DefaultNavigator.java b/src/main/java/org/olat/ims/qti/navigator/DefaultNavigator.java
index 16bd832384c..ec40fdf0d7b 100644
--- a/src/main/java/org/olat/ims/qti/navigator/DefaultNavigator.java
+++ b/src/main/java/org/olat/ims/qti/navigator/DefaultNavigator.java
@@ -45,16 +45,20 @@ public class DefaultNavigator implements Serializable {
 	private AssessmentInstance assessmentInstance;
 
 	private Info info;
+	private transient NavigatorDelegate delegate;
 
 	/**
 	 * 
 	 */
-	public DefaultNavigator(AssessmentInstance assessmentInstance) {
+	public DefaultNavigator(AssessmentInstance assessmentInstance, NavigatorDelegate delegate) {
 		this.assessmentInstance = assessmentInstance;
+		this.delegate = delegate;
 		info = new Info();
 	}
 	
-	
+	public void setDelegate(NavigatorDelegate delegate) {
+		this.delegate = delegate;
+	}
 
 	/**
 	 * @return AssessmentContext
@@ -138,11 +142,12 @@ public class DefaultNavigator implements Serializable {
 		}
 		return sectionResult;
 	}
+	
 
 	/**
 	 * @see org.olat.qti.process.Navigator#submitAssessment()
 	 */
-	public void submitAssessment() {
+	public final void submitAssessment() {
 		Output pendingOutput = null;
 		boolean pendingFeedback = getInfo().isFeedback();
 		boolean alreadyClosed = getAssessmentInstance().isClosed();
@@ -171,14 +176,22 @@ public class DefaultNavigator implements Serializable {
 		info.setMessage(QTIConstants.MESSAGE_ASSESSMENT_SUBMITTED);
 		info.setStatus(QTIConstants.ASSESSMENT_FINISHED);
 		info.setRenderItems(false);
+		
+		if(delegate != null) {
+			delegate.submitAssessment(assessmentInstance);
+		}
 	}
 
-	public void cancelAssessment() {
+	public final void cancelAssessment() {
 		getAssessmentInstance().close();
 		info.clear();
 		info.setMessage(QTIConstants.MESSAGE_ASSESSMENT_CANCELED);
 		info.setStatus(QTIConstants.ASSESSMENT_CANCELED);
 		info.setRenderItems(false);
+		
+		if(delegate != null) {
+			delegate.cancelAssessment(assessmentInstance);
+		}
 	}
 	
 	/**
diff --git a/src/main/java/org/olat/ims/qti/navigator/MenuItemNavigator.java b/src/main/java/org/olat/ims/qti/navigator/MenuItemNavigator.java
index 7db758687c3..42702177c76 100644
--- a/src/main/java/org/olat/ims/qti/navigator/MenuItemNavigator.java
+++ b/src/main/java/org/olat/ims/qti/navigator/MenuItemNavigator.java
@@ -47,8 +47,8 @@ public class MenuItemNavigator extends DefaultNavigator implements Navigator, Se
 	/**
 	 * @param assessmentContext
 	 */
-	public MenuItemNavigator(AssessmentInstance assessmentInstance) {
-		super(assessmentInstance);
+	public MenuItemNavigator(AssessmentInstance assessmentInstance, NavigatorDelegate delegate) {
+		super(assessmentInstance, delegate);
 	}
 
 	public void startAssessment() {
diff --git a/src/main/java/org/olat/ims/qti/navigator/MenuSectionNavigator.java b/src/main/java/org/olat/ims/qti/navigator/MenuSectionNavigator.java
index 1341bdd7204..8e064934c96 100644
--- a/src/main/java/org/olat/ims/qti/navigator/MenuSectionNavigator.java
+++ b/src/main/java/org/olat/ims/qti/navigator/MenuSectionNavigator.java
@@ -46,8 +46,8 @@ public class MenuSectionNavigator extends DefaultNavigator implements Navigator,
 	/**
 	 * @param assessmentContext
 	 */
-	public MenuSectionNavigator(AssessmentInstance assessmentInstance) {
-		super(assessmentInstance);
+	public MenuSectionNavigator(AssessmentInstance assessmentInstance, NavigatorDelegate delegate) {
+		super(assessmentInstance, delegate);
 	}
 
 	public void startAssessment() {
diff --git a/src/main/java/org/olat/ims/qti/navigator/Navigator.java b/src/main/java/org/olat/ims/qti/navigator/Navigator.java
index e9c18d14a4a..1f3a2e3852b 100644
--- a/src/main/java/org/olat/ims/qti/navigator/Navigator.java
+++ b/src/main/java/org/olat/ims/qti/navigator/Navigator.java
@@ -41,4 +41,5 @@ public interface Navigator {
 	public void goToSection(int sectionPos);
 
 	public Info getInfo();
+	public void setDelegate(NavigatorDelegate delegate);
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti/navigator/NavigatorDelegate.java b/src/main/java/org/olat/ims/qti/navigator/NavigatorDelegate.java
new file mode 100644
index 00000000000..326b6511cd9
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti/navigator/NavigatorDelegate.java
@@ -0,0 +1,34 @@
+/**
+ * <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.navigator;
+
+import org.olat.ims.qti.process.AssessmentInstance;
+
+/**
+ * 
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ */
+public interface NavigatorDelegate {
+	
+	public void submitAssessment(AssessmentInstance ai);
+	
+	public void cancelAssessment(AssessmentInstance ai);
+
+}
diff --git a/src/main/java/org/olat/ims/qti/navigator/SequentialItemNavigator.java b/src/main/java/org/olat/ims/qti/navigator/SequentialItemNavigator.java
index 1b43ecc1762..a4f6de42501 100644
--- a/src/main/java/org/olat/ims/qti/navigator/SequentialItemNavigator.java
+++ b/src/main/java/org/olat/ims/qti/navigator/SequentialItemNavigator.java
@@ -55,8 +55,8 @@ public class SequentialItemNavigator extends DefaultNavigator implements Navigat
 	/**
 	 * @param assessmentInstance
 	 */
-	public SequentialItemNavigator(AssessmentInstance assessmentInstance) {
-		super(assessmentInstance);
+	public SequentialItemNavigator(AssessmentInstance assessmentInstance, NavigatorDelegate delegate) {
+		super(assessmentInstance, delegate);
 	}
 
 	/**
diff --git a/src/main/java/org/olat/ims/qti/navigator/SequentialSectionNavigator.java b/src/main/java/org/olat/ims/qti/navigator/SequentialSectionNavigator.java
index d567f101ae4..3997e44540b 100644
--- a/src/main/java/org/olat/ims/qti/navigator/SequentialSectionNavigator.java
+++ b/src/main/java/org/olat/ims/qti/navigator/SequentialSectionNavigator.java
@@ -54,8 +54,8 @@ public class SequentialSectionNavigator extends DefaultNavigator implements Navi
 	/**
 	 * @param assessmentInstance
 	 */
-	public SequentialSectionNavigator(AssessmentInstance assessmentInstance) {
-		super(assessmentInstance);
+	public SequentialSectionNavigator(AssessmentInstance assessmentInstance, NavigatorDelegate delegate) {
+		super(assessmentInstance, delegate);
 	}
 
 	public void startAssessment() {
diff --git a/src/main/java/org/olat/ims/qti/process/AssessmentFactory.java b/src/main/java/org/olat/ims/qti/process/AssessmentFactory.java
index b9466cf893c..b231244be1b 100644
--- a/src/main/java/org/olat/ims/qti/process/AssessmentFactory.java
+++ b/src/main/java/org/olat/ims/qti/process/AssessmentFactory.java
@@ -30,6 +30,7 @@ import org.olat.core.logging.Tracing;
 import org.olat.core.util.CodeHelper;
 import org.olat.core.util.StringHelper;
 import org.olat.course.nodes.iq.IQEditController;
+import org.olat.ims.qti.navigator.NavigatorDelegate;
 import org.olat.modules.ModuleConfiguration;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryManager;
@@ -47,7 +48,7 @@ public class AssessmentFactory {
 	 * @return
 	 */
 	public static AssessmentInstance createAssessmentInstance(Identity subj, String remoteAddr, ModuleConfiguration modConfig, boolean preview,
-			long callingResId, String callingResDetail, String resourcePathInfo) {
+			long callingResId, String callingResDetail, String resourcePathInfo, NavigatorDelegate delegate) {
 		AssessmentInstance ai = null;
 		Persister persister = null;
 
@@ -74,7 +75,7 @@ public class AssessmentFactory {
 			Resolver resolver = new ImsRepositoryResolver(re.getKey());
 			long aiID = CodeHelper.getForeverUniqueID();
 			try {
-				ai = new AssessmentInstance(subj, remoteAddr, re.getKey().longValue(), aiID, callingResId, callingResDetail, resolver, persister, modConfig);
+				ai = new AssessmentInstance(subj, remoteAddr, re.getKey().longValue(), aiID, callingResId, callingResDetail, resolver, persister, modConfig, delegate);
 			} catch (Exception e) { return null; }
 		}
 		else {
@@ -86,6 +87,7 @@ public class AssessmentFactory {
 			ai.setAssessedIdentity(subj);
 			ai.setCallingResId(callingResId);
 			ai.setCallingResDetail(callingResDetail);
+			ai.setDelegate(delegate);
 			if(StringHelper.containsNonWhitespace(ai.getRemoteAddr())) {
 				ai.setRemoteAddr(remoteAddr);
 			}
@@ -101,9 +103,10 @@ public class AssessmentFactory {
 	 * @param doc
 	 * @return
 	 */
-	public static AssessmentInstance createAssessmentInstance(Identity subj, String remoteAddr, long callingResId, String callingResDetail, Resolver resolver, Persister persister, ModuleConfiguration modConfig) {
+	public static AssessmentInstance createAssessmentInstance(Identity subj, String remoteAddr, long callingResId, String callingResDetail, Resolver resolver, Persister persister,
+			ModuleConfiguration modConfig, NavigatorDelegate delegate) {
 		long aiID = CodeHelper.getForeverUniqueID();
-		return new AssessmentInstance(null, remoteAddr, 0, aiID, callingResId, callingResDetail, resolver, persister, modConfig);
+		return new AssessmentInstance(null, remoteAddr, 0, aiID, callingResId, callingResDetail, resolver, persister, modConfig, delegate);
 	}
 
 }
diff --git a/src/main/java/org/olat/ims/qti/process/AssessmentInstance.java b/src/main/java/org/olat/ims/qti/process/AssessmentInstance.java
index e990a288a61..80832407922 100644
--- a/src/main/java/org/olat/ims/qti/process/AssessmentInstance.java
+++ b/src/main/java/org/olat/ims/qti/process/AssessmentInstance.java
@@ -39,6 +39,7 @@ import org.olat.ims.qti.container.SectionContext;
 import org.olat.ims.qti.navigator.MenuItemNavigator;
 import org.olat.ims.qti.navigator.MenuSectionNavigator;
 import org.olat.ims.qti.navigator.Navigator;
+import org.olat.ims.qti.navigator.NavigatorDelegate;
 import org.olat.ims.qti.navigator.SequentialItemNavigator;
 import org.olat.ims.qti.navigator.SequentialSectionNavigator;
 import org.olat.modules.ModuleConfiguration;
@@ -139,7 +140,7 @@ public class AssessmentInstance implements Serializable {
 	 * @param modConfig
 	 */
 	public AssessmentInstance(Identity identity, String remoteAddr, long repositoryEntryKey, long assessID, long callingResId, String callingResDetail,
-			Resolver resolver, Persister persistor, ModuleConfiguration modConfig) {
+			Resolver resolver, Persister persistor, ModuleConfiguration modConfig, NavigatorDelegate delegate) {
 		this.assessedIdentity = identity;
 		this.remoteAddr = remoteAddr;
 		this.callingResId = callingResId;
@@ -211,7 +212,7 @@ public class AssessmentInstance implements Serializable {
 		
 		assessmentContext = new AssessmentContext();
 		assessmentContext.setUp(this);
-		createNavigator();
+		createNavigator(delegate);
 	}
 
 	public Identity getAssessedIdentity() {
@@ -274,21 +275,21 @@ public class AssessmentInstance implements Serializable {
 	public boolean isSelfAssess() { return type == TYPE_SELF;	}
 	public boolean isSurvey() { return type == TYPE_SURVEY;	}
 	
-	private void createNavigator() {
+	private void createNavigator(NavigatorDelegate delegate) {
 		if (menu) {
 			if (sequence == SEQUENCE_SECTION) {
-				navigator = new MenuSectionNavigator(this);
+				navigator = new MenuSectionNavigator(this, delegate);
 			} 
 			else {
-				navigator = new MenuItemNavigator(this);
+				navigator = new MenuItemNavigator(this, delegate);
 			}
 		}
 		else { // not menu
 			if (sequence == SEQUENCE_SECTION) {
-				navigator = new SequentialSectionNavigator(this);
+				navigator = new SequentialSectionNavigator(this, delegate);
 			} 
 			else {
-				navigator = new SequentialItemNavigator(this);
+				navigator = new SequentialItemNavigator(this, delegate);
 			}
 		}
 	}
@@ -514,6 +515,12 @@ public class AssessmentInstance implements Serializable {
 		preview = b;
 	}
 	
+	public void setDelegate(NavigatorDelegate delegate) {
+		if(navigator != null) {
+			navigator.setDelegate(delegate);
+		}
+	}
+	
 	/*
 	 * For marking/flagging question items ... OLAT-5807
 	 */
diff --git a/src/main/java/org/olat/modules/iq/IQDisplayController.java b/src/main/java/org/olat/modules/iq/IQDisplayController.java
index 6dc21fc7666..998e3fbd89d 100644
--- a/src/main/java/org/olat/modules/iq/IQDisplayController.java
+++ b/src/main/java/org/olat/modules/iq/IQDisplayController.java
@@ -28,6 +28,7 @@ package org.olat.modules.iq;
 import java.io.File;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 
 import org.apache.log4j.Logger;
@@ -65,6 +66,7 @@ import org.olat.ims.qti.container.AssessmentContext;
 import org.olat.ims.qti.container.ItemsInput;
 import org.olat.ims.qti.container.SectionContext;
 import org.olat.ims.qti.navigator.Navigator;
+import org.olat.ims.qti.navigator.NavigatorDelegate;
 import org.olat.ims.qti.process.AssessmentFactory;
 import org.olat.ims.qti.process.AssessmentInstance;
 import org.olat.ims.qti.process.FilePersister;
@@ -79,7 +81,7 @@ import org.olat.util.logging.activity.LoggingResourceable;
 /**
  * @author Felix Jost
  */
-public class IQDisplayController extends DefaultController implements GenericEventListener, Activateable2 {
+public class IQDisplayController extends DefaultController implements GenericEventListener, Activateable2, NavigatorDelegate {
 
 	private static final String PACKAGE = Util.getPackageName(IQDisplayController.class);
 	private static final String VELOCITY_ROOT = Util.getPackageVelocityRoot(IQDisplayController.class);
@@ -92,8 +94,11 @@ public class IQDisplayController extends DefaultController implements GenericEve
 	private String repositorySoftkey = null;
 	private Resolver resolver = null;
 	private Persister persister = null;
+	private final Locale locale;
 	private final Identity assessedIdentity;
 	private volatile boolean retrievedFlag = false;
+	
+	private NavigatorDelegate delegate;
 
 	private ProgressBar qtiscoreprogress, qtiquestionprogress;
 	private IQComponent qticomp;
@@ -123,10 +128,12 @@ public class IQDisplayController extends DefaultController implements GenericEve
 	 * @param callingResDetail
 	 */
 	IQDisplayController(ModuleConfiguration moduleConfiguration, IQSecurityCallback secCallback, UserRequest ureq,
-			WindowControl wControl, long callingResId, String callingResDetail) {
+			WindowControl wControl, long callingResId, String callingResDetail, NavigatorDelegate delegate) {
 		super(wControl);
 		
 		this.assessedIdentity = ureq.getIdentity();
+		this.locale = ureq.getLocale();
+		this.delegate = delegate;
 
 		ThreadLocalUserActivityLogger.log(LearningResourceLoggingAction.LEARNING_RESOURCE_OPEN, getClass());
 
@@ -157,6 +164,7 @@ public class IQDisplayController extends DefaultController implements GenericEve
 		ThreadLocalUserActivityLogger.log(LearningResourceLoggingAction.LEARNING_RESOURCE_OPEN, getClass());
 
 		this.assessedIdentity = ureq.getIdentity();
+		this.locale = ureq.getLocale();
 		this.modConfig = new ModuleConfiguration();
 		modConfig.set(IQEditController.CONFIG_KEY_ENABLEMENU, Boolean.TRUE);
 		modConfig.set(IQEditController.CONFIG_KEY_TYPE, type);
@@ -252,26 +260,16 @@ public class IQDisplayController extends DefaultController implements GenericEve
 
 		// get the assessment
 		AssessmentInstance ai = null;
-		//
-		// IQManagers synchronizes display controller creation with qti editor, please see comment in qti editor
-		//
-		//synchronized (QTIEditorMainController.IS_SAVING) {
-		//QTIEditorMainController.IS_SAVING_RWL.readLock().lock();
-		//lock is now checked in the IQManager -> see there
-		//try{
-			if (repositorySoftkey != null) { // instantiate from repository
-				// build path information which will be used to store tempory qti file
-				String resourcePathInfo = callingResId + File.separator + callingResDetail; 
-				ai = AssessmentFactory.createAssessmentInstance(ureq.getIdentity(), ureq.getHttpReq().getRemoteAddr(),
-						modConfig, iqsec.isPreview(), callingResId, callingResDetail, resourcePathInfo); 
-			} else if (resolver != null) { // instantiate from given resolver
-				ai = AssessmentFactory.createAssessmentInstance(ureq.getIdentity(), ureq.getHttpReq().getRemoteAddr(),
-						callingResId, callingResDetail, resolver, persister, modConfig);
-			}
-		//}finally{
-			//QTIEditorMainController.IS_SAVING_RWL.readLock().unlock();
-		//}		
-		//}
+		if (repositorySoftkey != null) { // instantiate from repository
+			// build path information which will be used to store tempory qti file
+			String resourcePathInfo = callingResId + File.separator + callingResDetail; 
+			ai = AssessmentFactory.createAssessmentInstance(ureq.getIdentity(), ureq.getHttpReq().getRemoteAddr(),
+					modConfig, iqsec.isPreview(), callingResId, callingResDetail, resourcePathInfo, this); 
+		} else if (resolver != null) { // instantiate from given resolver
+			ai = AssessmentFactory.createAssessmentInstance(ureq.getIdentity(), ureq.getHttpReq().getRemoteAddr(),
+					callingResId, callingResDetail, resolver, persister, modConfig, this);
+		}
+
 		// check for null instance or instance with no items
 		if (ai == null || ai.getAssessmentContext().getSectionContext(0).getItemContextCount() == 0) throw new AssertException(
 				"Assessment Instance was null or no sections/items found.");
@@ -523,12 +521,11 @@ public class IQDisplayController extends DefaultController implements GenericEve
 		}
 	}
 	
-	/**
-	 * Persist data in all cases: test, selftest, surveys except previews
-	 * In case of survey, data will be anonymized when reading from the
-	 * table (using the archiver)
-	 */
-	protected void postSubmitAssessment(UserRequest ureq, AssessmentInstance ai) {
+
+	
+	
+	@Override
+	public void submitAssessment(AssessmentInstance ai) {
 		if (!qtistatus.isPreview()) {
 			//iqm.persistResults(ai, callingResId, callingResDetail, ureq.getIdentity(), ureq.getHttpReq().getRemoteAddr());
 			getWindowControl().setInfo(translator.translate("status.results.saved"));
@@ -536,25 +533,37 @@ public class IQDisplayController extends DefaultController implements GenericEve
 			getWindowControl().setInfo(translator.translate("status.results.notsaved"));
 		}
 
-		if (!qtistatus.isSurvey()) {
+		if (!qtistatus.isSurvey() && !iqsec.isPreview()) {
 			// for test and self-assessment, generate detailed results
-			generateDetailsResults(ureq, ai);
-		} else {
+			Document docResReporting = iqm.getResultsReporting(ai, assessedIdentity, locale);
+			FilePersister.createResultsReporting(docResReporting, assessedIdentity, ai.getFormattedType(), ai.getAssessID());
+		}
+		
+		if(delegate != null) {
+			delegate.submitAssessment(ai);
+		}
+	}
+
+	@Override
+	public void cancelAssessment(AssessmentInstance ai) {
+		//
+	}
+
+	/**
+	 * Persist data in all cases: test, selftest, surveys except previews
+	 * In case of survey, data will be anonymized when reading from the
+	 * table (using the archiver)
+	 */
+	protected void postSubmitAssessment(UserRequest ureq, AssessmentInstance ai) {
+		if (qtistatus.isSurvey()) {
 			// Send also finished event in case of survey
 			fireEvent(ureq, new IQSubmittedEvent());
 		}
 	}
 	
 	protected void generateDetailsResults(UserRequest ureq, AssessmentInstance ai) {
-		Document docResReporting = iqm.getResultsReporting(ai, ureq.getIdentity(), ureq.getLocale());
 		if (!iqsec.isPreview()) {
-			FilePersister.createResultsReporting(docResReporting, ureq.getIdentity(), ai.getFormattedType(), ai.getAssessID());
-			// Send score and passed to parent controller. Maybe it is necessary
-			// to save some data there
-			// Do this now and not later, maybe user will never click on
-			// 'close'...
-			AssessmentContext ac = ai.getAssessmentContext();
-			fireEvent(ureq, new IQSubmittedEvent(ac.getScore(), ac.isPassed(), ai.getAssessID()));
+			fireEvent(ureq, new IQSubmittedEvent());
 		}
 		
 		Boolean showResultsOnFinishObj = (Boolean)modConfig.get(IQEditController.CONFIG_KEY_RESULT_ON_FINISH);
@@ -563,13 +572,12 @@ public class IQDisplayController extends DefaultController implements GenericEve
 			// do not display results reporting
 			myContent.contextPut("displayreporting", Boolean.FALSE);
 		} else { // display results reporting
+			Document docResReporting = iqm.getResultsReporting(ai, ureq.getIdentity(), ureq.getLocale());
 			String resReporting = iqm.transformResultsReporting(docResReporting, ureq.getLocale(), ai.getSummaryType() );
 			myContent.contextPut("resreporting", resReporting);
 			myContent.contextPut("displayreporting", Boolean.TRUE);
 		} 
 		myContent.setPage(VELOCITY_ROOT + "/result.html");
-		
-		
 	}
 
 	/**
diff --git a/src/main/java/org/olat/modules/iq/IQManager.java b/src/main/java/org/olat/modules/iq/IQManager.java
index 378debd9b1c..82d22632b5f 100644
--- a/src/main/java/org/olat/modules/iq/IQManager.java
+++ b/src/main/java/org/olat/modules/iq/IQManager.java
@@ -78,6 +78,7 @@ import org.olat.ims.qti.container.ItemContext;
 import org.olat.ims.qti.container.ItemInput;
 import org.olat.ims.qti.container.ItemsInput;
 import org.olat.ims.qti.container.SectionContext;
+import org.olat.ims.qti.navigator.NavigatorDelegate;
 import org.olat.ims.qti.process.AssessmentInstance;
 import org.olat.ims.qti.process.FilePersister;
 import org.olat.ims.qti.process.Resolver;
@@ -121,7 +122,7 @@ public class IQManager extends BasicManager implements UserDataDeletable {
 	 *  
 	 */
 	public Controller createIQDisplayController(ModuleConfiguration moduleConfiguration, IQSecurityCallback secCallback, UserRequest ureq,
-			WindowControl wControl, long callingResId, String callingResDetail) {
+			WindowControl wControl, long callingResId, String callingResDetail, NavigatorDelegate delegate) {
 		
 		//two cases:
 		// -- VERY RARE CASE -- 1) qti is open in an editor session right now on the screen (or session on the way to timeout)
@@ -136,7 +137,7 @@ public class IQManager extends BasicManager implements UserDataDeletable {
 					translator.translate("status.currently.locked", new String[] {lockResult.getOwner().getName()}));
 		}else{
 			ThreadLocalUserActivityLogger.addLoggingResourceInfo(LoggingResourceable.wrap(re, OlatResourceableType.iq));
-			return new IQDisplayController(moduleConfiguration, secCallback, ureq, wControl, callingResId, callingResDetail);
+			return new IQDisplayController(moduleConfiguration, secCallback, ureq, wControl, callingResId, callingResDetail, delegate);
 		}
 	}
 
diff --git a/src/main/java/org/olat/modules/iq/IQSubmittedEvent.java b/src/main/java/org/olat/modules/iq/IQSubmittedEvent.java
index cf0c891063e..5f046aae691 100644
--- a/src/main/java/org/olat/modules/iq/IQSubmittedEvent.java
+++ b/src/main/java/org/olat/modules/iq/IQSubmittedEvent.java
@@ -33,10 +33,8 @@ import org.olat.core.gui.control.Event;
  * @author gnaegi
  */
 public class IQSubmittedEvent extends Event {
-	private float score = 0;
-	private boolean passed = false;
-	private long assessmentID;
 	
+	private static final long serialVersionUID = -7510782040615263505L;
 
 	/**
 	 * constructor for a finished survey event
@@ -44,38 +42,4 @@ public class IQSubmittedEvent extends Event {
 	public IQSubmittedEvent() {
 		super("iqfinished");
 	}
-
-	/**
-	 * Constructor for a finished test or selftest event
-	 * @param score
-	 * @param passed
-	 */
-	public IQSubmittedEvent(float score, boolean passed, long assessmentID) {
-		super("iqfinished");
-		this.score = score;
-		this.passed = passed;
-		this.assessmentID = assessmentID;
-	}
-
-	
-	/**
-	 * @return Returns the passed.
-	 */
-	public boolean isPassed() {
-		return passed;
-	}
-	/**
-	 * @return Returns the score.
-	 */
-	public float getScore() {
-		return score;
-	}
-
-	/**
-	 * 
-	 * @return Returns the (last) assessmentID.
-	 */
-	public long getAssessmentID() {
-		return assessmentID;
-	}
 }
diff --git a/src/main/java/org/olat/restapi/repository/course/CourseAssessmentWebService.java b/src/main/java/org/olat/restapi/repository/course/CourseAssessmentWebService.java
index f41508d5be2..5d9d205955d 100644
--- a/src/main/java/org/olat/restapi/repository/course/CourseAssessmentWebService.java
+++ b/src/main/java/org/olat/restapi/repository/course/CourseAssessmentWebService.java
@@ -362,7 +362,7 @@ public class CourseAssessmentWebService {
 				// The consequence is that we must loop on section and items and set the
 				// navigator on
 				// the right position before submitting the inputs.
-				AssessmentInstance ai = AssessmentFactory.createAssessmentInstance(identity, "", modConfig, false, course.getResourceableId(), courseNode.getIdent(), resourcePathInfo);
+				AssessmentInstance ai = AssessmentFactory.createAssessmentInstance(identity, "", modConfig, false, course.getResourceableId(), courseNode.getIdent(), resourcePathInfo, null);
 				Navigator navigator = ai.getNavigator();
 				navigator.startAssessment();
 				// The type of the navigator depends on the setting of the course node
-- 
GitLab