/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.ims.qti21.ui; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.testFeedbackVisible; import java.io.File; import java.math.BigDecimal; import java.net.URI; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; import org.olat.core.gui.components.form.flexible.impl.elements.FormSubmit; import org.olat.core.gui.components.htmlheader.jscss.JSAndCSSComponent; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.panel.StackedPanel; import org.olat.core.gui.components.progressbar.ProgressBarItem; 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.core.gui.control.generic.modal.DialogBoxController; import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.id.context.BusinessControlFactory; import org.olat.core.id.context.ContextEntry; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.UserSession; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.resource.OresHelper; import org.olat.fileresource.FileResourceManager; import org.olat.fileresource.types.ImsQTI21Resource; import org.olat.fileresource.types.ImsQTI21Resource.PathResourceLocator; import org.olat.ims.qti21.AssessmentItemSession; import org.olat.ims.qti21.AssessmentResponse; import org.olat.ims.qti21.AssessmentSessionAuditLogger; import org.olat.ims.qti21.AssessmentTestHelper; import org.olat.ims.qti21.AssessmentTestMarks; import org.olat.ims.qti21.AssessmentTestSession; import org.olat.ims.qti21.OutcomesListener; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.QTI21DeliveryOptions; import org.olat.ims.qti21.QTI21Module; import org.olat.ims.qti21.QTI21Service; import org.olat.ims.qti21.manager.ResponseFormater; import org.olat.ims.qti21.model.DigitalSignatureOptions; import org.olat.ims.qti21.model.InMemoryAssessmentTestMarks; import org.olat.ims.qti21.model.ParentPartItemRefs; import org.olat.ims.qti21.model.ResponseLegality; import org.olat.ims.qti21.model.audit.CandidateEvent; import org.olat.ims.qti21.model.audit.CandidateExceptionReason; import org.olat.ims.qti21.model.audit.CandidateItemEventType; import org.olat.ims.qti21.model.audit.CandidateTestEventType; import org.olat.ims.qti21.ui.ResponseInput.Base64Input; import org.olat.ims.qti21.ui.ResponseInput.FileInput; import org.olat.ims.qti21.ui.ResponseInput.StringInput; import org.olat.ims.qti21.ui.components.AssessmentTestFormItem; import org.olat.ims.qti21.ui.components.AssessmentTreeFormItem; import org.olat.ims.qti21.ui.event.RetrieveAssessmentTestSessionEvent; import org.olat.modules.assessment.AssessmentEntry; import org.olat.modules.assessment.AssessmentService; import org.olat.repository.RepositoryEntry; import org.olat.util.logging.activity.LoggingResourceable; import org.springframework.beans.factory.annotation.Autowired; import uk.ac.ed.ph.jqtiplus.JqtiPlus; import uk.ac.ed.ph.jqtiplus.exception.QtiCandidateStateException; import uk.ac.ed.ph.jqtiplus.exception.ResponseBindingException; import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; import uk.ac.ed.ph.jqtiplus.node.result.AssessmentResult; import uk.ac.ed.ph.jqtiplus.node.result.ItemResult; import uk.ac.ed.ph.jqtiplus.node.result.ItemVariable; import uk.ac.ed.ph.jqtiplus.node.result.OutcomeVariable; import uk.ac.ed.ph.jqtiplus.node.result.TestResult; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest; import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode; import uk.ac.ed.ph.jqtiplus.node.test.SubmissionMode; import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback; import uk.ac.ed.ph.jqtiplus.node.test.TestFeedbackAccess; import uk.ac.ed.ph.jqtiplus.node.test.TestPart; import uk.ac.ed.ph.jqtiplus.notification.NotificationLevel; import uk.ac.ed.ph.jqtiplus.notification.NotificationRecorder; import uk.ac.ed.ph.jqtiplus.provision.BadResourceException; import uk.ac.ed.ph.jqtiplus.reading.QtiModelBuildingError; import uk.ac.ed.ph.jqtiplus.reading.QtiXmlInterpretationException; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest; import uk.ac.ed.ph.jqtiplus.running.ItemProcessingContext; import uk.ac.ed.ph.jqtiplus.running.ItemSessionController; import uk.ac.ed.ph.jqtiplus.running.TestPlanVisitor; import uk.ac.ed.ph.jqtiplus.running.TestPlanner; import uk.ac.ed.ph.jqtiplus.running.TestProcessingInitializer; import uk.ac.ed.ph.jqtiplus.running.TestSessionController; import uk.ac.ed.ph.jqtiplus.running.TestSessionControllerSettings; import uk.ac.ed.ph.jqtiplus.state.ItemSessionState; import uk.ac.ed.ph.jqtiplus.state.TestPartSessionState; import uk.ac.ed.ph.jqtiplus.state.TestPlan; import uk.ac.ed.ph.jqtiplus.state.TestPlanNode; import uk.ac.ed.ph.jqtiplus.state.TestPlanNode.TestNodeType; import uk.ac.ed.ph.jqtiplus.state.TestPlanNodeKey; import uk.ac.ed.ph.jqtiplus.state.TestProcessingMap; import uk.ac.ed.ph.jqtiplus.state.TestSessionState; import uk.ac.ed.ph.jqtiplus.types.FileResponseData; import uk.ac.ed.ph.jqtiplus.types.Identifier; import uk.ac.ed.ph.jqtiplus.types.ResponseData; import uk.ac.ed.ph.jqtiplus.types.StringResponseData; import uk.ac.ed.ph.jqtiplus.value.BooleanValue; import uk.ac.ed.ph.jqtiplus.value.FloatValue; import uk.ac.ed.ph.jqtiplus.value.IntegerValue; import uk.ac.ed.ph.jqtiplus.value.NumberValue; import uk.ac.ed.ph.jqtiplus.value.Value; import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator; /** * * Initial date: 08.12.2014<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class AssessmentTestDisplayController extends BasicController implements CandidateSessionContext, GenericEventListener { private final File fUnzippedDirRoot; private String mapperUri; private final QTI21DeliveryOptions deliveryOptions; private final QTI21OverrideOptions overrideOptions; private VelocityContainer mainVC; private final StackedPanel mainPanel; private QtiWorksController qtiWorksCtrl; private AssessmentResultController resultCtrl; private TestSessionController testSessionController; private DialogBoxController advanceTestPartDialog, endTestPartDialog; private DialogBoxController confirmCancelDialog; private DialogBoxController confirmSuspendDialog; private CandidateEvent lastEvent; private Date currentRequestTimestamp; private AssessmentTestSession candidateSession; private ResolvedAssessmentTest resolvedAssessmentTest; private AssessmentTestMarks marks; private RepositoryEntry testEntry; private RepositoryEntry entry; private String subIdent; private final boolean anonym; private final Identity assessedIdentity; private final String anonymousIdentifier; private final boolean showCloseResults; private OutcomesListener outcomesListener; private AssessmentSessionAuditLogger candidateAuditLogger; @Autowired private QTI21Module qtiModule; @Autowired private QTI21Service qtiService; @Autowired private AssessmentService assessmentService; /** * * @param ureq * @param wControl * @param listener If the listener is null, the controller will use the default listener which save the score and pass in assessment entry * @param testEntry * @param entry * @param subIdent * @param deliveryOptions * @param showCloseResults set to false prevent the close results button to appears (this boolean * don't change the settings to show or not the results at the end of the test) * @param authorMode if true, the database objects are not counted and can be deleted without warning */ public AssessmentTestDisplayController(UserRequest ureq, WindowControl wControl, OutcomesListener listener, RepositoryEntry testEntry, RepositoryEntry entry, String subIdent, QTI21DeliveryOptions deliveryOptions, QTI21OverrideOptions overrideOptions, boolean showCloseResults, boolean authorMode, boolean anonym) { super(ureq, wControl); this.entry = entry; this.subIdent = subIdent; this.testEntry = testEntry; this.outcomesListener = listener; this.deliveryOptions = deliveryOptions; this.overrideOptions = overrideOptions; this.showCloseResults = showCloseResults; UserSession usess = ureq.getUserSession(); if(usess.getRoles().isGuestOnly() || anonym) { this.anonym = anonym; assessedIdentity = null; anonymousIdentifier = getAnonymousIdentifier(usess); } else { this.anonym = anonym; assessedIdentity = getIdentity(); anonymousIdentifier = null; } if(testEntry == entry) { // Limit to the case where the test is launched as resource, // within course is this task delegated to the QTI21AssessmentRunController addLoggingResourceable(LoggingResourceable.wrapTest(entry)); } FileResourceManager frm = FileResourceManager.getInstance(); fUnzippedDirRoot = frm.unzipFileResource(testEntry.getOlatResource()); resolvedAssessmentTest = qtiService.loadAndResolveAssessmentTest(fUnzippedDirRoot, false, false); if(resolvedAssessmentTest == null || resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful() == null) { mainVC = createVelocityContainer("error"); } else { currentRequestTimestamp = ureq.getRequestTimestamp(); initMarks(); initOrResumeAssessmentTestSession(ureq, authorMode); URI assessmentObjectUri = qtiService.createAssessmentTestUri(fUnzippedDirRoot); File submissionDir = qtiService.getSubmissionDirectory(candidateSession); mapperUri = registerCacheableMapper(ureq, "QTI21Resources::" + testEntry.getKey(), new ResourcesMapper(assessmentObjectUri, submissionDir)); /* Handle immediate end of test session */ if (testSessionController.getTestSessionState() != null && testSessionController.getTestSessionState().isEnded()) { immediateEndTestSession(ureq); mainVC = createVelocityContainer("end"); } else { mainVC = createVelocityContainer("run"); initQtiWorks(ureq); } OLATResourceable sessionOres = OresHelper .createOLATResourceableInstance(AssessmentTestSession.class, candidateSession.getKey()); CoordinatorManager.getInstance().getCoordinator().getEventBus().registerFor(this, getIdentity(), sessionOres); } mainPanel = putInitialPanel(mainVC); } private void immediateEndTestSession(UserRequest ureq) { AssessmentResult assessmentResult = qtiService.getAssessmentResult(candidateSession); if(assessmentResult == null) { candidateSession.setTerminationTime(ureq.getRequestTimestamp()); candidateSession.setExploded(true); candidateSession = qtiService.updateAssessmentTestSession(candidateSession); } else { qtiService.finishTestSession(candidateSession, testSessionController.getTestSessionState(), assessmentResult, currentRequestTimestamp, getDigitalSignatureOptions(), getIdentity()); } } private String getAnonymousIdentifier(UserSession usess) { String sessionId = usess.getSessionInfo().getSession().getId(); String testKey = (entry == null ? testEntry.getKey() : entry.getKey()) + "-" + subIdent +"-" + testEntry.getKey() + "-" + sessionId; Object id = usess.getEntry(testKey); if(id instanceof String) { return (String)id; } String newId = UUID.randomUUID().toString(); usess.putEntryInNonClearedStore(testKey, newId); return newId; } private void initMarks() { if(anonym) { marks = new InMemoryAssessmentTestMarks(); } else { marks = qtiService.getMarks(getIdentity(), entry, subIdent, testEntry); } } private void initOrResumeAssessmentTestSession(UserRequest ureq, boolean authorMode) { AssessmentEntry assessmentEntry = assessmentService.getOrCreateAssessmentEntry(assessedIdentity, anonymousIdentifier, entry, subIdent, testEntry); if(outcomesListener == null) { boolean manualCorrections = AssessmentTestHelper.needManualCorrection(resolvedAssessmentTest); outcomesListener = new AssessmentEntryOutcomesListener(assessmentEntry, manualCorrections, assessmentService, authorMode); } AssessmentTestSession lastSession = qtiService.getResumableAssessmentTestSession(assessedIdentity, anonymousIdentifier, entry, subIdent, testEntry, authorMode); if(lastSession == null) { candidateSession = qtiService.createAssessmentTestSession(assessedIdentity, anonymousIdentifier, assessmentEntry, entry, subIdent, testEntry, authorMode); candidateAuditLogger = qtiService.getAssessmentSessionAuditLogger(candidateSession, authorMode); testSessionController = enterSession(ureq); } else { candidateSession = lastSession; candidateAuditLogger = qtiService.getAssessmentSessionAuditLogger(candidateSession, authorMode); lastEvent = new CandidateEvent(candidateSession, testEntry, entry); lastEvent.setTestEventType(CandidateTestEventType.ITEM_EVENT); testSessionController = resumeSession(ureq); } } private void initQtiWorks(UserRequest ureq) { qtiWorksCtrl = new QtiWorksController(ureq, getWindowControl()); listenTo(qtiWorksCtrl); mainVC.put("qtirun", qtiWorksCtrl.getInitialComponent()); } @Override protected void doDispose() { suspendAssessmentTest(new Date()); OLATResourceable sessionOres = OresHelper .createOLATResourceableInstance(AssessmentTestSession.class, candidateSession.getKey()); CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(this, sessionOres); } @Override public boolean isTerminated() { return candidateSession.getTerminationTime() != null; } @Override public AssessmentTestSession getCandidateSession() { return candidateSession; } @Override public CandidateEvent getLastEvent() { return lastEvent; } @Override public Date getCurrentRequestTimestamp() { return currentRequestTimestamp; } public boolean isResultsVisible() { return qtiWorksCtrl.isResultsVisible(); } @Override public void event(Event event) { if(event instanceof RetrieveAssessmentTestSessionEvent) { RetrieveAssessmentTestSessionEvent rats = (RetrieveAssessmentTestSessionEvent)event; if(candidateSession != null && candidateSession.getKey().equals(rats.getAssessmentTestSessionKey())) { candidateSession = qtiService.reloadAssessmentTestSession(candidateSession); } } } @Override protected void event(UserRequest ureq, Component source, Event event) { // } @Override protected void event(UserRequest ureq, Controller source, Event event) { if(advanceTestPartDialog == source) { if(DialogBoxUIFactory.isOkEvent(event) || DialogBoxUIFactory.isYesEvent(event)) { processAdvanceTestPart(ureq); } mainVC.setDirty(true); } else if(endTestPartDialog == source) { if(DialogBoxUIFactory.isOkEvent(event) || DialogBoxUIFactory.isYesEvent(event)) { processEndTestPart(ureq); } mainVC.setDirty(true); } else if(confirmCancelDialog == source) { if(DialogBoxUIFactory.isOkEvent(event) || DialogBoxUIFactory.isYesEvent(event)) { doCancel(ureq); } } else if(confirmSuspendDialog == source) { if(DialogBoxUIFactory.isOkEvent(event) || DialogBoxUIFactory.isYesEvent(event)) { doSuspend(ureq); } } else if(qtiWorksCtrl == source) { if(event == Event.CANCELLED_EVENT) { doConfirmCancel(ureq); } else if("suspend".equals(event.getCommand())) { doConfirmSuspend(ureq); } else if(QTI21Event.CLOSE_RESULTS.equals(event.getCommand())) { fireEvent(ureq, event); } else if(event instanceof QTIWorksAssessmentTestEvent) { processQTIEvent(ureq, (QTIWorksAssessmentTestEvent)event); } } super.event(ureq, source, event); } private void doExitTest(UserRequest ureq) { fireEvent(ureq, new QTI21Event(QTI21Event.EXIT)); } private void doCloseResults(UserRequest ureq) { fireEvent(ureq, new QTI21Event(QTI21Event.CLOSE_RESULTS)); } private void doConfirmSuspend(UserRequest ureq) { String title = translate("suspend.test"); String text = translate("confirm.suspend.test"); confirmSuspendDialog = activateOkCancelDialog(ureq, title, text, confirmSuspendDialog); } private void doSuspend(UserRequest ureq) { VelocityContainer suspendedVC = createVelocityContainer("suspended"); mainPanel.setContent(suspendedVC); suspendAssessmentTest(ureq.getRequestTimestamp()); fireEvent(ureq, new Event("suspend")); } /** * It suspend the current item * @return */ private boolean suspendAssessmentTest(Date requestTimestamp) { if(!deliveryOptions.isEnableSuspend() || testSessionController == null || testSessionController.getTestSessionState() == null || testSessionController.getTestSessionState().isEnded() || testSessionController.getTestSessionState().isExited() || testSessionController.getTestSessionState().isSuspended()) { return false; } testSessionController.touchDurations(currentRequestTimestamp); testSessionController.suspendTestSession(requestTimestamp); TestSessionState testSessionState = testSessionController.getTestSessionState(); TestPlanNodeKey currentItemKey = testSessionState.getCurrentItemKey(); if(currentItemKey == null) { return false; } TestPlanNode currentItemNode = testSessionState.getTestPlan().getNode(currentItemKey); ItemProcessingContext itemProcessingContext = testSessionController.getItemProcessingContext(currentItemNode); ItemSessionState itemSessionState = itemProcessingContext.getItemSessionState(); if(itemProcessingContext instanceof ItemSessionController && !itemSessionState.isEnded() && !itemSessionState.isExited() && itemSessionState.isOpen() && !itemSessionState.isSuspended()) { ItemSessionController itemSessionController = (ItemSessionController)itemProcessingContext; itemSessionController.suspendItemSession(requestTimestamp); computeAndRecordTestAssessmentResult(requestTimestamp, testSessionState, false); NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); final CandidateEvent candidateEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.SUSPEND, null, null, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateEvent); this.lastEvent = candidateEvent; return true; } return false; } private void doConfirmCancel(UserRequest ureq) { String title = translate("cancel.test"); String text = translate("confirm.cancel.test"); confirmCancelDialog = activateOkCancelDialog(ureq, title, text, confirmCancelDialog); } private void doCancel(UserRequest ureq) { VelocityContainer cancelledVC = createVelocityContainer("cancelled"); mainPanel.setContent(cancelledVC); TestSessionState testSessionState = testSessionController.getTestSessionState(); qtiService.cancelTestSession(candidateSession, testSessionState); fireEvent(ureq, Event.CANCELLED_EVENT); } private boolean timeLimitBarrier(UserRequest ureq) { Long assessmentTestMaxTimeLimits = getAssessmentTestMaxTimeLimit(); if(assessmentTestMaxTimeLimits != null) { long maximumAssessmentTestDuration = assessmentTestMaxTimeLimits.longValue() * 1000;//convert in milliseconds TestSessionState testSessionState = testSessionController.getTestSessionState(); if(!testSessionState.isEnded() && !testSessionState.isExited()) { long durationMillis = testSessionState.getDurationAccumulated(); durationMillis += getRequestTimeStampDifferenceToNow(); if(durationMillis > maximumAssessmentTestDuration) { currentRequestTimestamp = ureq.getRequestTimestamp(); processExitTestAfterTimeLimit(ureq); return true; } } /* ItemProcessingContext ctx = testSessionController.getItemProcessingContext(null); ItemSessionState itemSessionState = ctx.getItemSessionState(); itemSessionState.getDurationAccumulated(); double itemDuration = ctx.getItemSessionState().computeDuration(); */ } return false; } /** * * @return The maximum time limit in seconds. */ private Long getAssessmentTestMaxTimeLimit() { if(overrideOptions != null && overrideOptions.getAssessmentTestMaxTimeLimit() != null) { Long timeLimits = overrideOptions.getAssessmentTestMaxTimeLimit(); return timeLimits.longValue() > 0 ? timeLimits : null; } AssessmentTest assessmentTest = resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful(); if(assessmentTest.getTimeLimits() != null && assessmentTest.getTimeLimits().getMaximum() != null) { return assessmentTest.getTimeLimits().getMaximum().longValue(); } return null; } private long getRequestTimeStampDifferenceToNow() { long diff = 0l; if(currentRequestTimestamp != null) { //take time between 2 reloads if the user reload the page diff = (new Date().getTime() - currentRequestTimestamp.getTime()); if(diff < 0) { diff = 0; } } return diff; } private void processQTIEvent(UserRequest ureq, QTIWorksAssessmentTestEvent qe) { if(timeLimitBarrier(ureq)) { return;// } currentRequestTimestamp = ureq.getRequestTimestamp(); switch(qe.getEvent()) { case selectItem: processSelectItem(ureq, qe.getSubCommand()); break; case nextItem: processNextItem(ureq); break; case finishItem: processFinishLinearItem(ureq); break; case reviewItem: processReviewItem(ureq, qe.getSubCommand()); break; case itemSolution: processItemSolution(ureq, qe.getSubCommand()); break; case testPartNavigation: processTestPartNavigation(ureq); break; case response: handleResponse(ureq, qe.getStringResponseMap(), qe.getFileResponseMap(), qe.getComment()); nextItemIfAllowed(ureq); break; case endTestPart: confirmEndTestPart(ureq); break; case advanceTestPart: confirmAdvanceTestPart(ureq); break; case reviewTestPart: processReviewTestPart(); break; case exitTest: processExitTest(ureq); break; case timesUp: processExitTestAfterTimeLimit(ureq); break; case tmpResponse: handleTemporaryResponse(ureq, qe.getStringResponseMap()); break; case source: logError("QtiWorks event source not implemented", null); break; case state: logError("QtiWorks event state not implemented", null); break; case result: logError("QtiWorks event result not implemented", null); break; case validation: logError("QtiWorks event validation not implemented", null); break; case authorview: logError("QtiWorks event authorview not implemented", null); break; case mark: toogleMark(qe.getSubCommand()); break; } } private void toogleMark(String itemRef) { if(marks == null) { if(anonym) { marks = new InMemoryAssessmentTestMarks(itemRef); } else { marks = qtiService.createMarks(assessedIdentity, entry, subIdent, testEntry, itemRef); } } else { String currentMarks = marks.getMarks(); if(currentMarks == null) { marks.setMarks(itemRef); } else if(currentMarks.indexOf(itemRef) >= 0) { marks.setMarks(currentMarks.replace(itemRef, "")); } else { marks.setMarks(currentMarks + " " + itemRef); } marks = qtiService.updateMarks(marks); } } @Override public boolean isMarked(String itemKey) { if(marks == null || marks.getMarks() == null) return false; return marks.getMarks().indexOf(itemKey) >= 0; } private void processSelectItem(UserRequest ureq, String key) { TestPlanNodeKey nodeKey = TestPlanNodeKey.fromString(key); Date requestTimestamp = ureq.getRequestTimestamp(); TestPlanNode selectedNode = testSessionController.selectItemNonlinear(requestTimestamp, nodeKey); /* Record and log event */ TestPlanNodeKey selectedNodeKey = (selectedNode == null ? null : selectedNode.getKey()); NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateEvent candidateEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.SELECT_MENU, null, selectedNodeKey, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateEvent); } /** * Try to go to the next item. It will check fi the current * item want to show some feedback (modal or element), has some * bad or invalid responses, state of the test session... or if * the item is an adaptive one. * * @param ureq */ private void nextItemIfAllowed(UserRequest ureq) { if(testSessionController.hasFollowingNonLinearItem() && testSessionController.getTestSessionState() != null && !testSessionController.getTestSessionState().isEnded() && !testSessionController.getTestSessionState().isExited()) { TestSessionState testSessionState = testSessionController.getTestSessionState(); TestPlanNodeKey itemNodeKey = testSessionState.getCurrentItemKey(); if(itemNodeKey != null) { TestPlanNode currentItemNode = testSessionState.getTestPlan().getNode(itemNodeKey); boolean hasFeedbacks = qtiWorksCtrl.willShowSomeAssessmentItemFeedbacks(currentItemNode); //allow skipping if(!hasFeedbacks) { processNextItem(ureq); } } } } private void processNextItem(UserRequest ureq) { Date requestTimestamp = ureq.getRequestTimestamp(); if(testSessionController.hasFollowingNonLinearItem()) { TestPlanNode selectedNode = testSessionController.selectFollowingItemNonLinear(requestTimestamp); TestPlanNodeKey selectedNodeKey = (selectedNode == null ? null : selectedNode.getKey()); NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateEvent candidateEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.NEXT_ITEM, null, selectedNodeKey, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateEvent); } } private void processReviewItem(UserRequest ureq, String key) { TestPlanNodeKey itemKey = TestPlanNodeKey.fromString(key); //Assert.notNull(itemKey, "itemKey"); NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); /* Make sure caller may do this */ //assertSessionNotTerminated(candidateSession); try { if (!testSessionController.mayReviewItem(itemKey)) { logError("CANNOT_REVIEW_TEST_ITEM", null); candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_REVIEW_TEST_ITEM, null); return; } } catch (final QtiCandidateStateException e) { logError("CANNOT_REVIEW_TEST_ITEM", e); candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_REVIEW_TEST_ITEM, e); return; } catch (final RuntimeException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_REVIEW_TEST_ITEM, e); logError("CANNOT_REVIEW_TEST_ITEM", e); return;// handleExplosion(e, candidateSession); } /* Record current result state */ computeAndRecordTestAssessmentResult(ureq.getRequestTimestamp(), testSessionState, false); /* Record and log event */ final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.REVIEW_ITEM, null, itemKey, testSessionState, notificationRecorder); this.lastEvent = candidateTestEvent; candidateAuditLogger.logCandidateEvent(candidateTestEvent); } private void processItemSolution(UserRequest ureq, String key) { TestPlanNodeKey itemKey = TestPlanNodeKey.fromString(key); NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); /* Make sure caller may do this */ //assertSessionNotTerminated(candidateSession); try { if (!testSessionController.mayAccessItemSolution(itemKey)) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_SOLUTION_TEST_ITEM, null); logError("CANNOT_SOLUTION_TEST_ITEM", null); return; } } catch (final QtiCandidateStateException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_SOLUTION_TEST_ITEM, e); logError("CANNOT_SOLUTION_TEST_ITEM", e); return; } catch (final RuntimeException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_SOLUTION_TEST_ITEM, e); logError("Exploded", e); return;// handleExplosion(e, candidateSession); } /* Record current result state */ computeAndRecordTestAssessmentResult(ureq.getRequestTimestamp(), testSessionState, false); /* Record and log event */ CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.SOLUTION_ITEM, null, itemKey, testSessionState, notificationRecorder); this.lastEvent = candidateTestEvent; candidateAuditLogger.logCandidateEvent(candidateTestEvent); } //public CandidateSession finishLinearItem(final CandidateSessionContext candidateSessionContext) // throws CandidateException { private void processFinishLinearItem(UserRequest ureq) { NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); try { if (!testSessionController.mayAdvanceItemLinear()) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_FINISH_LINEAR_TEST_ITEM, null); logError("CANNOT_FINISH_LINEAR_TEST_ITEM", null); return; } } catch (QtiCandidateStateException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_FINISH_LINEAR_TEST_ITEM, e); logError("CANNOT_FINISH_LINEAR_TEST_ITEM", e); return; } catch (RuntimeException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_FINISH_LINEAR_TEST_ITEM, e); logError("CANNOT_FINISH_LINEAR_TEST_ITEM", e); return;// handleExplosion(e, candidateSession); } // Update state final Date requestTimestamp = ureq.getRequestTimestamp(); final TestPlanNode nextItemNode = testSessionController.advanceItemLinear(requestTimestamp); //boolean terminated = nextItemNode == null && testSessionController.findNextEnterableTestPart() == null; // Record current result state final AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(requestTimestamp, testSessionState, false); /* If we ended the testPart and there are now no more available testParts, then finish the session now */ if (nextItemNode==null && testSessionController.findNextEnterableTestPart()==null) { candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, requestTimestamp, getDigitalSignatureOptions(), getIdentity()); } // Record and log event final CandidateTestEventType eventType = nextItemNode!=null ? CandidateTestEventType.FINISH_ITEM : CandidateTestEventType.FINISH_FINAL_ITEM; final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, eventType, null, null, testSessionState, notificationRecorder); this.lastEvent = candidateTestEvent; candidateAuditLogger.logCandidateEvent(candidateTestEvent); } private void processTestPartNavigation(UserRequest ureq) { final Date requestTimestamp = ureq.getRequestTimestamp(); testSessionController.selectItemNonlinear(requestTimestamp, null); } private ParentPartItemRefs getParentSection(TestPlanNodeKey itemKey) { TestSessionState testSessionState = testSessionController.getTestSessionState(); return AssessmentTestHelper.getParentSection(itemKey, testSessionState, resolvedAssessmentTest); } private void handleTemporaryResponse(UserRequest ureq, Map<Identifier, ResponseInput> stringResponseMap) { NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); final Date timestamp = ureq.getRequestTimestamp(); final Map<Identifier, ResponseData> responseDataMap = new HashMap<>(); if (stringResponseMap != null) { for (final Entry<Identifier, ResponseInput> responseEntry : stringResponseMap.entrySet()) { final Identifier identifier = responseEntry.getKey(); final ResponseInput responseData = responseEntry.getValue(); if(responseData instanceof StringInput) { responseDataMap.put(identifier, new StringResponseData(((StringInput)responseData).getResponseData())); } } } TestPlanNodeKey currentItemKey = testSessionState.getCurrentItemKey(); ParentPartItemRefs parentParts = getParentSection(currentItemKey); String assessmentItemIdentifier = currentItemKey.getIdentifier().toString(); AssessmentItemSession itemSession = qtiService .getOrCreateAssessmentItemSession(candidateSession, parentParts, assessmentItemIdentifier); TestPlanNode currentItemRefNode = testSessionState.getTestPlan().getNode(currentItemKey); ItemSessionController itemSessionController = (ItemSessionController)testSessionController .getItemProcessingContext(currentItemRefNode); ItemSessionState itemSessionState = itemSessionController.getItemSessionState(); List<Interaction> interactions = itemSessionController.getInteractions(); Map<Identifier,Interaction> interactionMap = new HashMap<>(); for(Interaction interaction:interactions) { interactionMap.put(interaction.getResponseIdentifier(), interaction); } Map<Identifier, AssessmentResponse> candidateResponseMap = qtiService.getAssessmentResponses(itemSession); for (Entry<Identifier, ResponseData> responseEntry : responseDataMap.entrySet()) { Identifier responseIdentifier = responseEntry.getKey(); ResponseData responseData = responseEntry.getValue(); AssessmentResponse candidateItemResponse; if(candidateResponseMap.containsKey(responseIdentifier)) { candidateItemResponse = candidateResponseMap.get(responseIdentifier); } else { candidateItemResponse = qtiService .createAssessmentResponse(candidateSession, itemSession, responseIdentifier.toString(), ResponseLegality.VALID, responseData.getType()); } switch (responseData.getType()) { case STRING: { List<String> data = ((StringResponseData) responseData).getResponseData(); String stringuifiedResponse = ResponseFormater.format(data); candidateItemResponse.setStringuifiedResponse(stringuifiedResponse); break; } default: throw new OLATRuntimeException("Unexpected switch case: " + responseData.getType()); } candidateResponseMap.put(responseIdentifier, candidateItemResponse); itemSessionState.setRawResponseData(responseIdentifier, responseData); try { Interaction interaction = interactionMap.get(responseIdentifier); interaction.bindResponse(itemSessionController, responseData); } catch (final ResponseBindingException e) { // } } /* Copy uncommitted responses over */ for (final Entry<Identifier, Value> uncommittedResponseEntry : itemSessionState.getUncommittedResponseValues().entrySet()) { final Identifier identifier = uncommittedResponseEntry.getKey(); final Value value = uncommittedResponseEntry.getValue(); itemSessionState.setResponseValue(identifier, value); } /* Persist CandidateResponse entities */ qtiService.recordTestAssessmentResponses(itemSession, candidateResponseMap.values()); /* Record resulting event */ final CandidateEvent candidateEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.ITEM_EVENT, null, currentItemKey, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateEvent, candidateResponseMap); /* Record current result state */ AssessmentResult assessmentResult = computeTestAssessmentResult(timestamp, candidateSession); synchronized(this) { qtiService.recordTestAssessmentResult(candidateSession, testSessionState, assessmentResult, candidateAuditLogger); } } //public CandidateSession handleResponses(final CandidateSessionContext candidateSessionContext, // final Map<Identifier, StringResponseData> stringResponseMap, // final Map<Identifier, MultipartFile> fileResponseMap, // final String candidateComment) private void handleResponse(UserRequest ureq, Map<Identifier, ResponseInput> stringResponseMap, Map<Identifier, ResponseInput> fileResponseMap, String candidateComment) { NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); final Map<Identifier,File> fileSubmissionMap = new HashMap<>(); final Map<Identifier, ResponseData> responseDataMap = new HashMap<>(); if (stringResponseMap != null) { for (final Entry<Identifier, ResponseInput> responseEntry : stringResponseMap.entrySet()) { final Identifier identifier = responseEntry.getKey(); final ResponseInput responseData = responseEntry.getValue(); if(responseData instanceof StringInput) { responseDataMap.put(identifier, new StringResponseData(((StringInput)responseData).getResponseData())); } else if(responseData instanceof Base64Input) { //only used from drawing interaction Base64Input fileInput = (Base64Input)responseData; String filename = "submitted_image.png"; File storedFile = qtiService.importFileSubmission(candidateSession, filename, fileInput.getResponseData()); responseDataMap.put(identifier, new FileResponseData(storedFile, fileInput.getContentType(), storedFile.getName())); fileSubmissionMap.put(identifier, storedFile); } else if(responseData instanceof FileInput) { FileInput fileInput = (FileInput)responseData; File storedFile = qtiService.importFileSubmission(candidateSession, fileInput.getMultipartFileInfos()); responseDataMap.put(identifier, new FileResponseData(storedFile, fileInput.getContentType(), storedFile.getName())); fileSubmissionMap.put(identifier, storedFile); } } } TestPlanNodeKey currentItemKey = testSessionState.getCurrentItemKey(); ParentPartItemRefs parentParts = getParentSection(currentItemKey); String assessmentItemIdentifier = currentItemKey.getIdentifier().toString(); AssessmentItemSession itemSession = qtiService .getOrCreateAssessmentItemSession(candidateSession, parentParts, assessmentItemIdentifier); if (fileResponseMap!=null) { for (Entry<Identifier, ResponseInput> fileResponseEntry : fileResponseMap.entrySet()) { Identifier identifier = fileResponseEntry.getKey(); FileInput multipartFile = (FileInput)fileResponseEntry.getValue(); if (!multipartFile.isEmpty()) { File storedFile = qtiService.importFileSubmission(candidateSession, multipartFile.getMultipartFileInfos()); responseDataMap.put(identifier, new FileResponseData(storedFile, multipartFile.getContentType(), storedFile.getName())); fileSubmissionMap.put(identifier, storedFile); } } } Map<Identifier, AssessmentResponse> candidateResponseMap = qtiService.getAssessmentResponses(itemSession); for (Entry<Identifier, ResponseData> responseEntry : responseDataMap.entrySet()) { Identifier responseIdentifier = responseEntry.getKey(); ResponseData responseData = responseEntry.getValue(); AssessmentResponse candidateItemResponse; if(candidateResponseMap.containsKey(responseIdentifier)) { candidateItemResponse = candidateResponseMap.get(responseIdentifier); } else { candidateItemResponse = qtiService .createAssessmentResponse(candidateSession, itemSession, responseIdentifier.toString(), ResponseLegality.VALID, responseData.getType()); } switch (responseData.getType()) { case STRING: { List<String> data = ((StringResponseData) responseData).getResponseData(); String stringuifiedResponse = ResponseFormater.format(data); candidateItemResponse.setStringuifiedResponse(stringuifiedResponse); break; } case FILE: { if(fileSubmissionMap.get(responseIdentifier) != null) { File storedFile = fileSubmissionMap.get(responseIdentifier); candidateItemResponse.setStringuifiedResponse(storedFile.getName()); } break; } default: throw new OLATRuntimeException("Unexpected switch case: " + responseData.getType()); } candidateResponseMap.put(responseIdentifier, candidateItemResponse); } boolean allResponsesValid = true; boolean allResponsesBound = true; final Date timestamp = ureq.getRequestTimestamp(); if (candidateComment != null) { testSessionController.setCandidateCommentForCurrentItem(timestamp, candidateComment); } /* Attempt to bind responses (and maybe perform RP & OP) */ testSessionController.handleResponsesToCurrentItem(timestamp, responseDataMap); /* Classify this event */ final SubmissionMode submissionMode = testSessionController.getCurrentTestPart().getSubmissionMode(); final CandidateItemEventType candidateItemEventType; if (allResponsesValid) { candidateItemEventType = submissionMode == SubmissionMode.INDIVIDUAL ? CandidateItemEventType.ATTEMPT_VALID : CandidateItemEventType.RESPONSE_VALID; } else { candidateItemEventType = allResponsesBound ? CandidateItemEventType.RESPONSE_INVALID : CandidateItemEventType.RESPONSE_BAD; } /* Record resulting event */ final CandidateEvent candidateEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.ITEM_EVENT, candidateItemEventType, currentItemKey, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateEvent, candidateResponseMap); this.lastEvent = candidateEvent; /* Record current result state */ AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(timestamp, testSessionState, false); ItemSessionState itemSessionState = testSessionState.getCurrentItemSessionState(); long itemDuration = itemSessionState.getDurationAccumulated(); itemSession.setDuration(itemDuration); ItemResult itemResult = assessmentResult.getItemResult(assessmentItemIdentifier); collectOutcomeVariablesForItemSession(itemResult, itemSession); /* Persist CandidateResponse entities */ qtiService.recordTestAssessmentResponses(itemSession, candidateResponseMap.values()); /* Save any change to session state */ candidateSession = qtiService.updateAssessmentTestSession(candidateSession); } private void collectOutcomeVariablesForItemSession(ItemResult resultNode, AssessmentItemSession itemSession) { BigDecimal score = null; Boolean pass = null; for (final ItemVariable itemVariable : resultNode.getItemVariables()) { if (itemVariable instanceof OutcomeVariable) { OutcomeVariable outcomeVariable = (OutcomeVariable)itemVariable; Identifier identifier = outcomeVariable.getIdentifier(); if(QTI21Constants.SCORE_IDENTIFIER.equals(identifier)) { Value value = itemVariable.getComputedValue(); if(value instanceof FloatValue) { score = new BigDecimal(((FloatValue)value).doubleValue()); } else if(value instanceof IntegerValue) { score = new BigDecimal(((IntegerValue)value).intValue()); } } else if(QTI21Constants.PASS_IDENTIFIER.equals(identifier)) { Value value = itemVariable.getComputedValue(); if(value instanceof BooleanValue) { pass = ((BooleanValue)value).booleanValue(); } } } } if(score != null) { itemSession.setScore(score); } if(pass != null) { itemSession.setPassed(pass); } } private void confirmEndTestPart(UserRequest ureq) { TestPlanNode nextTestPart = testSessionController.findNextEnterableTestPart(); if(nextTestPart == null) { String title = translate("confirm.finish.test.title"); String text = translate("confirm.finish.test.text"); endTestPartDialog = activateOkCancelDialog(ureq, title, text, endTestPartDialog); } else { TestPart currentTestPart = testSessionController.getCurrentTestPart(); if(currentTestPart == null) { processEndTestPart(ureq); } else { String title = translate("confirm.finish.testpart.title"); String text = translate("confirm.finish.testpart.text"); endTestPartDialog = activateOkCancelDialog(ureq, title, text, endTestPartDialog); } } } //public CandidateSession endCurrentTestPart(final CandidateSessionContext candidateSessionContext) private void processEndTestPart(UserRequest ureq) { /* Update state */ final Date requestTimestamp = ureq.getRequestTimestamp(); testSessionController.endCurrentTestPart(requestTimestamp); TestSessionState testSessionState = testSessionController.getTestSessionState(); TestPlanNode nextTestPart = testSessionController.findNextEnterableTestPart(); // Record current result state final AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(requestTimestamp, testSessionState, nextTestPart == null); if(nextTestPart == null) { candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, requestTimestamp, getDigitalSignatureOptions(), getIdentity()); if(!qtiWorksCtrl.willShowSomeAssessmentTestFeedbacks()) { //need feedback, no more parts, quickly exit try { //end current test part testSessionController.enterNextAvailableTestPart(requestTimestamp); } catch (final QtiCandidateStateException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e); logError("CANNOT_ADVANCE_TEST_PART", e); return; } catch (final RuntimeException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e); logError("RuntimeException", e); return;// handleExplosion(e, candidateSession); } //exit the test NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); CandidateTestEventType eventType = CandidateTestEventType.EXIT_TEST; testSessionController.exitTest(requestTimestamp); candidateSession.setTerminationTime(requestTimestamp); candidateSession = qtiService.updateAssessmentTestSession(candidateSession); /* Record and log event */ final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, eventType, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateTestEvent); this.lastEvent = candidateTestEvent; qtiWorksCtrl.updateStatusAndResults(ureq); doExitTest(ureq); } } else if(!qtiWorksCtrl.willShowSomeTestPartFeedbacks()) { //no feedback, go to the next part processAdvanceTestPart(ureq); } } /** * In the case of a multi-part test, the entry to the first part * must not be confirmed. * @param ureq */ private void confirmAdvanceTestPart(UserRequest ureq) { TestPlanNode nextTestPart = testSessionController.findNextEnterableTestPart(); if(nextTestPart == null) { String title = translate("confirm.close.test.title"); String text = translate("confirm.close.test.text"); advanceTestPartDialog = activateOkCancelDialog(ureq, title, text, advanceTestPartDialog); } else { TestPart currentTestPart = testSessionController.getCurrentTestPart(); if(currentTestPart == null) { processAdvanceTestPart(ureq); } else { String title = translate("confirm.advance.testpart.title"); String text = translate("confirm.advance.testpart.text"); advanceTestPartDialog = activateOkCancelDialog(ureq, title, text, advanceTestPartDialog); } } } private void processAdvanceTestPart(UserRequest ureq) { /* Get current JQTI state and create JQTI controller */ NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); /* Perform action */ final TestPlanNode nextTestPart; final Date currentTimestamp = ureq.getRequestTimestamp(); try { nextTestPart = testSessionController.enterNextAvailableTestPart(currentTimestamp); } catch (final QtiCandidateStateException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e); logError("CANNOT_ADVANCE_TEST_PART", e); return; } catch (final RuntimeException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e); logError("RuntimeException", e); return;// handleExplosion(e, candidateSession); } CandidateTestEventType eventType; if (nextTestPart!=null) { /* Moved into next test part */ eventType = CandidateTestEventType.ADVANCE_TEST_PART; } else { /* No more test parts. * * For single part tests, we terminate the test completely now as the test feedback was shown with the testPart feedback. * For multi-part tests, we shall keep the test open so that the test feedback can be viewed. */ if (testSessionState.getTestPlan().getTestPartNodes().size()==1) { eventType = CandidateTestEventType.EXIT_TEST; testSessionController.exitTest(currentTimestamp); candidateSession.setTerminationTime(currentTimestamp); candidateSession = qtiService.updateAssessmentTestSession(candidateSession); } else { eventType = CandidateTestEventType.ADVANCE_TEST_PART; } } boolean terminated = isTerminated(); /* Record current result state */ computeAndRecordTestAssessmentResult(currentTimestamp, testSessionState, terminated); /* Record and log event */ final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, eventType, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateTestEvent); this.lastEvent = candidateTestEvent; if (terminated) { qtiWorksCtrl.updateStatusAndResults(ureq); doExitTest(ureq); } } private void processReviewTestPart() { NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); /* Make sure caller may do this */ //assertSessionNotTerminated(candidateSession); if (testSessionState.getCurrentTestPartKey()==null || !testSessionState.getCurrentTestPartSessionState().isEnded()) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_REVIEW_TEST_PART, null); logError("CANNOT_REVIEW_TEST_PART", null); return; } /* Record and log event */ final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.REVIEW_TEST_PART, null, null, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateTestEvent); this.lastEvent = candidateTestEvent; } /** * Exit multi-part tests */ private void processExitTest(UserRequest ureq) { NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); /* Perform action */ final Date currentTimestamp = ureq.getRequestTimestamp(); try { testSessionController.exitTest(currentTimestamp); } catch (final QtiCandidateStateException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_EXIT_TEST, e); logError("CANNOT_EXIT_TEST", null); return; } catch (final RuntimeException e) { candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_EXIT_TEST, e); logError("Exploded", null); return;// handleExplosion(e, candidateSession); } /* Update CandidateSession as appropriate */ candidateSession.setTerminationTime(currentTimestamp); candidateSession = qtiService.updateAssessmentTestSession(candidateSession); /* Record current result state (final) */ computeAndRecordTestAssessmentResult(currentTimestamp, testSessionState, true); /* Record and log event */ final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.EXIT_TEST, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateTestEvent); this.lastEvent = candidateTestEvent; doExitTest(ureq); } private void processExitTestAfterTimeLimit(UserRequest ureq) { synchronized(testSessionController) { // make sure the ajax call and a user click don't close both the session NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionState testSessionState = testSessionController.getTestSessionState(); if(testSessionState.isEnded() || testSessionState.isExited()) return; //close duration testSessionController.touchDurations(currentRequestTimestamp); final Date requestTimestamp = ureq.getRequestTimestamp(); testSessionController.exitTestIncomplete(requestTimestamp); candidateSession.setTerminationTime(requestTimestamp); // Record current result state final AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(requestTimestamp, testSessionState, true); candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, requestTimestamp, getDigitalSignatureOptions(), getIdentity()); qtiWorksCtrl.updateStatusAndResults(ureq); /* Record and log event */ final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.EXIT_DUE_TIME_LIMIT, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateTestEvent); this.lastEvent = candidateTestEvent; } doExitTest(ureq); } //private CandidateSession enterCandidateSession(final CandidateSession candidateSession) private TestSessionController enterSession(UserRequest ureq) { /* Set up listener to record any notifications */ final NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); /* Create fresh JQTI+ state & controller for it */ testSessionController = createNewTestSessionStateAndController(notificationRecorder); if (testSessionController == null) { return null; } /* Initialise test state and enter test */ final TestSessionState testSessionState = testSessionController.getTestSessionState(); final Date timestamp = ureq.getRequestTimestamp(); try { testSessionController.initialize(timestamp); final int testPartCount = testSessionController.enterTest(timestamp); if (testPartCount==1) { /* If there is only testPart, then enter this (if possible). * (Note that this may cause the test to exit immediately if there is a failed * preCondition on this part.) */ testSessionController.enterNextAvailableTestPart(timestamp); } else { /* Don't enter first testPart yet - we shall tell candidate that * there are multiple parts and let them enter manually. */ } } catch (final RuntimeException e) { logError("", e); return null; } /* Record and log event */ final CandidateEvent candidateEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry, CandidateTestEventType.ENTER_TEST, testSessionState, notificationRecorder); candidateAuditLogger.logCandidateEvent(candidateEvent); this.lastEvent = candidateEvent; boolean ended = testSessionState.isEnded(); /* Record current result state */ final AssessmentResult assessmentResult = computeAndRecordTestAssessmentResult(timestamp, testSessionState, ended); /* Handle immediate end of test session */ if (ended) { qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult, timestamp, getDigitalSignatureOptions(), getIdentity()); } else { TestPart currentTestPart = testSessionController.getCurrentTestPart(); if(currentTestPart != null && currentTestPart.getNavigationMode() == NavigationMode.NONLINEAR) { //go to the first assessment item if(testSessionController.hasFollowingNonLinearItem()) { testSessionController.selectFollowingItemNonLinear(currentRequestTimestamp); } } } return testSessionController; } private DigitalSignatureOptions getDigitalSignatureOptions() { boolean sendMail = deliveryOptions.isDigitalSignatureMail(); boolean digitalSignature = deliveryOptions.isDigitalSignature() && qtiModule.isDigitalSignatureEnabled(); DigitalSignatureOptions options = new DigitalSignatureOptions(digitalSignature, sendMail, entry, testEntry); if(digitalSignature) { outcomesListener.decorateConfirmation(candidateSession, options, getCurrentRequestTimestamp(), getLocale()); } return options; } private TestSessionController createNewTestSessionStateAndController(NotificationRecorder notificationRecorder) { TestProcessingMap testProcessingMap = getTestProcessingMap(); /* Generate a test plan for this session */ final TestPlanner testPlanner = new TestPlanner(testProcessingMap); if (notificationRecorder!=null) { testPlanner.addNotificationListener(notificationRecorder); } final TestPlan testPlan = testPlanner.generateTestPlan(); final TestSessionState testSessionState = new TestSessionState(testPlan); final TestSessionControllerSettings testSessionControllerSettings = new TestSessionControllerSettings(); testSessionControllerSettings.setTemplateProcessingLimit(computeTemplateProcessingLimit()); testProcessingMap.reduceItemProcessingMapMap(testPlan.getTestPlanNodeList()); /* Create controller and wire up notification recorder */ final TestSessionController result = new TestSessionController(qtiService.jqtiExtensionManager(), testSessionControllerSettings, testProcessingMap, testSessionState); if (notificationRecorder!=null) { result.addNotificationListener(notificationRecorder); } return result; } private TestSessionController resumeSession(UserRequest ureq) { Date currentRequestTimestamp = ureq.getRequestTimestamp(); final NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO); TestSessionController controller = createTestSessionController(notificationRecorder); controller.unsuspendTestSession(currentRequestTimestamp); TestSessionState testSessionState = controller.getTestSessionState(); TestPlanNodeKey currentItemKey = testSessionState.getCurrentItemKey(); if(currentItemKey != null) { TestPlanNode currentItemNode = testSessionState.getTestPlan().getNode(currentItemKey); ItemProcessingContext itemProcessingContext = controller.getItemProcessingContext(currentItemNode); ItemSessionState itemSessionState = itemProcessingContext.getItemSessionState(); if(itemProcessingContext instanceof ItemSessionController && itemSessionState.isSuspended()) { ItemSessionController itemSessionController = (ItemSessionController)itemProcessingContext; itemSessionController.unsuspendItemSession(currentRequestTimestamp); } } return controller; } private TestSessionController createTestSessionController(NotificationRecorder notificationRecorder) { final TestSessionState testSessionState = qtiService.loadTestSessionState(candidateSession); return createTestSessionController(testSessionState, notificationRecorder); } public TestSessionController createTestSessionController(TestSessionState testSessionState, NotificationRecorder notificationRecorder) { /* Try to resolve the underlying JQTI+ object */ final TestProcessingMap testProcessingMap = getTestProcessingMap(); if (testProcessingMap == null) { return null; } /* Create config for TestSessionController */ final TestSessionControllerSettings testSessionControllerSettings = new TestSessionControllerSettings(); testSessionControllerSettings.setTemplateProcessingLimit(computeTemplateProcessingLimit()); /* Create controller and wire up notification recorder (if passed) */ final TestSessionController result = new TestSessionController(qtiService.jqtiExtensionManager(), testSessionControllerSettings, testProcessingMap, testSessionState); if (notificationRecorder!=null) { result.addNotificationListener(notificationRecorder); } return result; } private AssessmentResult computeAndRecordTestAssessmentResult(Date requestTimestamp, TestSessionState testSessionState, boolean submit) { AssessmentResult assessmentResult = computeTestAssessmentResult(requestTimestamp, candidateSession); synchronized(this) { qtiService.recordTestAssessmentResult(candidateSession, testSessionState, assessmentResult, candidateAuditLogger); } processOutcomeVariables(assessmentResult.getTestResult(), submit); return assessmentResult; } private void processOutcomeVariables(TestResult resultNode, boolean submit) { Float score = null; Boolean pass = null; for (final ItemVariable itemVariable : resultNode.getItemVariables()) { if (itemVariable instanceof OutcomeVariable) { OutcomeVariable outcomeVariable = (OutcomeVariable)itemVariable; Identifier identifier = outcomeVariable.getIdentifier(); if(QTI21Constants.SCORE_IDENTIFIER.equals(identifier)) { Value value = itemVariable.getComputedValue(); if(value instanceof NumberValue) { score = (float) ((NumberValue)value).doubleValue(); } } else if(QTI21Constants.PASS_IDENTIFIER.equals(identifier)) { Value value = itemVariable.getComputedValue(); if(value instanceof BooleanValue) { pass = ((BooleanValue)value).booleanValue(); } } } } if(submit) { outcomesListener.submit(score, pass, candidateSession.getKey()); } else { outcomesListener.updateOutcomes(score, pass); } } private AssessmentResult computeTestAssessmentResult(Date requestTimestamp, final AssessmentTestSession testSession) { List<ContextEntry> entries = getWindowControl().getBusinessControl().getEntries(); OLATResourceable testSessionOres = OresHelper.createOLATResourceableInstance("TestSession", testSession.getKey()); entries.add(BusinessControlFactory.getInstance().createContextEntry(testSessionOres)); String url = BusinessControlFactory.getInstance().getAsAuthURIString(entries, true); final URI sessionIdentifierSourceId = URI.create(url); final String sessionIdentifier = "testsession/" + testSession.getKey(); return testSessionController .computeAssessmentResult(requestTimestamp, sessionIdentifier, sessionIdentifierSourceId); } private TestProcessingMap getTestProcessingMap() { boolean assessmentPackageIsValid = true; BadResourceException ex = resolvedAssessmentTest.getTestLookup().getBadResourceException(); if(ex instanceof QtiXmlInterpretationException) { try {//try to log some informations QtiXmlInterpretationException exml = (QtiXmlInterpretationException)ex; logError(exml.getInterpretationFailureReason().toString(), null); for(QtiModelBuildingError err :exml.getQtiModelBuildingErrors()) { logError(err.toString(), null); } } catch (Exception e) { logError("", e); } } TestProcessingInitializer initializer = new TestProcessingInitializer(resolvedAssessmentTest, assessmentPackageIsValid); TestProcessingMap result = initializer.initialize(); return result; } /** * Request limit configured outer of the QTI 2.1 file. * @return */ public int computeTemplateProcessingLimit() { final Integer requestedLimit = deliveryOptions.getTemplateProcessingLimit(); if (requestedLimit == null) { /* Not specified, so use default */ return JqtiPlus.DEFAULT_TEMPLATE_PROCESSING_LIMIT; } final int requestedLimitIntValue = requestedLimit.intValue(); return requestedLimitIntValue > 0 ? requestedLimitIntValue : JqtiPlus.DEFAULT_TEMPLATE_PROCESSING_LIMIT; } /** * QtiWorks manage the form tag itself. * * Initial date: 20.05.2015<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ private class QtiWorksController extends AbstractQtiWorksController { private AssessmentTestFormItem qtiEl; private AssessmentTreeFormItem qtiTreeEl; private ProgressBarItem scoreProgress, questionProgress; private FormLink endTestPartButton, closeTestButton, cancelTestButton, suspendTestButton, closeResultsButton; private String menuWidth; private boolean resultsVisible = false; private final QtiWorksStatus qtiWorksStatus = new QtiWorksStatus(); public QtiWorksController(UserRequest ureq, WindowControl wControl) { super(ureq, wControl, "at_run"); initPreferences(ureq); initForm(ureq); } private void initPreferences(UserRequest ureq) { try { menuWidth = (String)ureq.getUserSession().getGuiPreferences() .get(this.getClass(), getMenuPrefsKey()); } catch (Exception e) { logError("", e); } } @Override protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { mainForm.setMultipartEnabled(true); FormSubmit submit = uifactory.addFormSubmitButton("submit", formLayout); submit.setElementCssClass("o_sel_assessment_item_submit"); qtiEl = new AssessmentTestFormItem("qtirun", submit); qtiEl.setResolvedAssessmentTest(resolvedAssessmentTest); formLayout.add("qtirun", qtiEl); boolean showMenuTree = deliveryOptions.isShowMenu(); qtiTreeEl = new AssessmentTreeFormItem("qtitree", qtiEl.getComponent(), submit); qtiTreeEl.setResolvedAssessmentTest(resolvedAssessmentTest); qtiTreeEl.setVisible(showMenuTree); formLayout.add("qtitree", qtiTreeEl); String endName = qtiEl.getComponent().hasMultipleTestParts() ? "assessment.test.end.testPart" : "assessment.test.end.test"; endTestPartButton = uifactory.addFormLink("endTest", endName, null, formLayout, Link.BUTTON); endTestPartButton.setForceOwnDirtyFormWarning(true); endTestPartButton.setElementCssClass("o_sel_end_testpart"); endTestPartButton.setPrimary(true); endTestPartButton.setIconLeftCSS("o_icon o_icon-fw o_icon_qti_end_testpart"); closeTestButton = uifactory.addFormLink("closeTest", "assessment.test.close.test", null, formLayout, Link.BUTTON); closeTestButton.setElementCssClass("o_sel_close_test"); closeTestButton.setPrimary(true); closeTestButton.setIconLeftCSS("o_icon o_icon-fw o_icon_qti_close_test"); if(deliveryOptions.isEnableCancel()) { cancelTestButton = uifactory.addFormLink("cancelTest", "cancel.test", null, formLayout, Link.BUTTON); cancelTestButton.setElementCssClass("o_sel_cancel_test"); cancelTestButton.setIconLeftCSS("o_icon o_icon-fw o_icon_qti_cancel"); } if(deliveryOptions.isEnableSuspend()) { suspendTestButton = uifactory.addFormLink("suspendTest", "suspend.test", null, formLayout, Link.BUTTON); suspendTestButton.setElementCssClass("o_sel_suspend_test"); suspendTestButton.setIconLeftCSS("o_icon o_icon-fw o_icon_qti_suspend"); } closeResultsButton = uifactory.addFormLink("closeResults", "assessment.test.close.results", null, formLayout, Link.BUTTON); closeResultsButton.setElementCssClass("o_sel_close_results"); closeResultsButton.setIconLeftCSS("o_icon o_icon-fw o_icon_qti_close_results"); closeResultsButton.setPrimary(true); closeResultsButton.setVisible(false); ResourceLocator fileResourceLocator = new PathResourceLocator(fUnzippedDirRoot.toPath()); final ResourceLocator inputResourceLocator = ImsQTI21Resource.createResolvingResourceLocator(fileResourceLocator); qtiEl.setResourceLocator(inputResourceLocator); qtiEl.setTestSessionController(testSessionController); qtiEl.setAssessmentObjectUri(qtiService.createAssessmentTestUri(fUnzippedDirRoot)); qtiEl.setCandidateSessionContext(AssessmentTestDisplayController.this); qtiEl.setMapperUri(mapperUri); qtiEl.setRenderNavigation(!showMenuTree); qtiEl.setPersonalNotes(deliveryOptions.isPersonalNotes()); qtiEl.setShowTitles(deliveryOptions.isShowTitles()); qtiTreeEl.setResourceLocator(inputResourceLocator); qtiTreeEl.setTestSessionController(testSessionController); qtiTreeEl.setAssessmentObjectUri(qtiService.createAssessmentTestUri(fUnzippedDirRoot)); qtiTreeEl.setCandidateSessionContext(AssessmentTestDisplayController.this); qtiTreeEl.setMapperUri(mapperUri); if(formLayout instanceof FormLayoutContainer) { FormLayoutContainer layoutCont = (FormLayoutContainer)formLayout; AssessmentTest assessmentTest = resolvedAssessmentTest.getRootNodeLookup().extractAssumingSuccessful(); layoutCont.contextPut("title", assessmentTest.getTitle()); layoutCont.contextPut("qtiWorksStatus", qtiWorksStatus); String[] jss = new String[] { "js/jquery/ui/jquery-ui-1.11.4.custom.resize.min.js", "js/jquery/qti/jquery.qtiTimer.js", "js/jquery/qti/jquery.qtiAutosave.js", }; JSAndCSSComponent js = new JSAndCSSComponent("js", jss, null); layoutCont.put("js", js); layoutCont.contextPut("displayScoreProgress", deliveryOptions.isDisplayScoreProgress()); layoutCont.contextPut("displayQuestionProgress", deliveryOptions.isDisplayQuestionProgress()); if(deliveryOptions.isDisplayScoreProgress()) { scoreProgress = uifactory.addProgressBar("scoreProgress", null, 100, 0, 0, "", formLayout); scoreProgress.setWidthInPercent(true); formLayout.add("", scoreProgress); } if(deliveryOptions.isDisplayQuestionProgress()) { questionProgress = uifactory.addProgressBar("questionProgress", null, 100, 0, 0, "", formLayout); questionProgress.setWidthInPercent(true); formLayout.add("questionProgress", questionProgress); } } flc.getFormItemComponent().addListener(this); if(StringHelper.containsNonWhitespace(menuWidth)) { flc.contextPut("menuWidth", menuWidth); } updateStatusAndResults(ureq); } public boolean willShowSomeAssessmentItemFeedbacks(TestPlanNode itemNode) { if(itemNode == null || testSessionController == null || testSessionController.getTestSessionState().isExited() || testSessionController.getTestSessionState().isEnded()) { return true; } return qtiEl.getComponent().willShowFeedbacks(itemNode); } /** * * @return */ public boolean willShowSomeAssessmentTestFeedbacks() { if(testSessionController == null || testSessionController.getTestSessionState().isExited() || testSessionController.getTestSessionState().isEnded()) { return true; } TestSessionState testSessionState = testSessionController.getTestSessionState(); TestPlanNodeKey currentTestPartNodeKey = testSessionState.getCurrentTestPartKey(); TestPlanNode currentTestPlanNode = testSessionState.getTestPlan().getNode(currentTestPartNodeKey); boolean hasReviewableItems = currentTestPlanNode.searchDescendants(TestNodeType.ASSESSMENT_ITEM_REF) .stream().anyMatch(itemNode -> itemNode.getEffectiveItemSessionControl().isAllowReview() || itemNode.getEffectiveItemSessionControl().isShowFeedback()); if(hasReviewableItems) { return true; } //Show 'atEnd' test feedback f there's only 1 testPart List<TestFeedback> testFeedbacks = qtiEl.getComponent().getAssessmentTest().getTestFeedbacks(); for(TestFeedback testFeedback:testFeedbacks) { if(testFeedback.getTestFeedbackAccess() == TestFeedbackAccess.AT_END && testFeedbackVisible(testFeedback, testSessionController.getTestSessionState())) { return true; } } return false; } /** * * Check if the current test part will show some test part feedback, * item feedback or item reviews. * * @return */ public boolean willShowSomeTestPartFeedbacks() { if(testSessionController == null || testSessionController.getTestSessionState().isExited() || testSessionController.getTestSessionState().isEnded()) { return true; } TestSessionState testSessionState = testSessionController.getTestSessionState(); TestPlanNodeKey currentTestPartNodeKey = testSessionState.getCurrentTestPartKey(); TestPlanNode currentTestPlanNode = testSessionState.getTestPlan().getNode(currentTestPartNodeKey); boolean hasReviewableItems = currentTestPlanNode.searchDescendants(TestNodeType.ASSESSMENT_ITEM_REF) .stream().anyMatch(itemNode -> itemNode.getEffectiveItemSessionControl().isAllowReview() || itemNode.getEffectiveItemSessionControl().isShowFeedback()); if(hasReviewableItems) { return true; } TestPart currentTestPart = testSessionController.getCurrentTestPart(); List<TestFeedback> testFeedbacks = currentTestPart.getTestFeedbacks(); for(TestFeedback testFeedback:testFeedbacks) { if(testFeedback.getTestFeedbackAccess() == TestFeedbackAccess.AT_END && testFeedbackVisible(testFeedback, testSessionController.getTestSessionState())) { return true; } } return false; } @Override protected Identifier getResponseIdentifierFromUniqueId(String uniqueId) { Interaction interaction = qtiEl.getInteractionOfResponseUniqueIdentifier(uniqueId); return interaction == null ? null : interaction.getResponseIdentifier(); } @Override protected void formOK(UserRequest ureq) { processResponse(ureq, qtiEl.getSubmitButton()); updateStatusAndResults(ureq); } @Override public void event(UserRequest ureq, Component source, Event event) { if(source == flc.getFormItemComponent()) { if("saveLeftColWidth".equals(event.getCommand())) { String width = ureq.getParameter("newEmWidth"); doSaveMenuWidth(ureq, width); } } super.event(ureq, source, event); } @Override protected void event(UserRequest ureq, Controller source, Event event) { if(source == resultCtrl) { fireEvent(ureq, event); } super.event(ureq, source, event); } @Override protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { if(closeResultsButton == source) { doCloseResults(ureq); } else if(!timeLimitBarrier(ureq)) { if(endTestPartButton == source) { doEndTestPart(ureq); } else if(closeTestButton == source) { doCloseTest(ureq); } else if(cancelTestButton == source) { doCancelTest(ureq); } else if(suspendTestButton == source) { doSuspendTest(ureq); } else if(source == qtiEl || source == qtiTreeEl) { if(event instanceof QTIWorksAssessmentTestEvent) { QTIWorksAssessmentTestEvent qwate = (QTIWorksAssessmentTestEvent)event; if(qwate.getEvent() == QTIWorksAssessmentTestEvent.Event.tmpResponse) { processTemporaryResponse(ureq); } else { fireEvent(ureq, event); } } } else if(source instanceof FormLink) { FormLink formLink = (FormLink)source; processResponse(ureq, formLink); } super.formInnerEvent(ureq, source, event); } updateStatusAndResults(ureq); mainForm.setDirtyMarking(false); mainForm.forceSubmittedAndValid(); } @Override protected void propagateDirtinessToContainer(FormItem fiSrc, FormEvent fe) { if(!"mark".equals(fe.getCommand())) { super.propagateDirtinessToContainer(fiSrc, fe); } } @Override protected void fireResponse(UserRequest ureq, FormItem source, Map<Identifier, ResponseInput> stringResponseMap, Map<Identifier, ResponseInput> fileResponseMap, String comment) { fireEvent(ureq, new QTIWorksAssessmentTestEvent(QTIWorksAssessmentTestEvent.Event.response, stringResponseMap, fileResponseMap, comment, source)); } @Override protected void fireTemporaryResponse(UserRequest ureq, Map<Identifier, ResponseInput> stringResponseMap) { fireEvent(ureq, new QTIWorksAssessmentTestEvent(QTIWorksAssessmentTestEvent.Event.tmpResponse, stringResponseMap, null, null, null)); } /** * Make sure that the end test part is not clicked 2x (which * the qtiworks runtime don't like). * * @param ureq */ private void doEndTestPart(UserRequest ureq) { TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = AssessmentTestDisplayController.this; if(!candidateSessionContext.isTerminated() && !testSessionState.isExited() && testSessionState.getCurrentTestPartKey() != null && testSessionController.mayEndCurrentTestPart()) { final TestPlanNodeKey currentTestPartKey = testSessionState.getCurrentTestPartKey(); final TestPartSessionState currentTestPartSessionState = testSessionState.getTestPartSessionStates().get(currentTestPartKey); if(!currentTestPartSessionState.isEnded()) { fireEvent(ureq, new QTIWorksAssessmentTestEvent(QTIWorksAssessmentTestEvent.Event.endTestPart, endTestPartButton)); qtiEl.getComponent().setDirty(true); qtiTreeEl.getComponent().setDirty(true); } } } /** * Make sure that the close test happens only once. * * @param ureq */ private void doCloseTest(UserRequest ureq) { TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = AssessmentTestDisplayController.this; if(!candidateSessionContext.isTerminated() && !testSessionState.isExited() && testSessionState.getCurrentTestPartKey() != null && testSessionController.mayEndCurrentTestPart()) { final TestPlanNodeKey currentTestPartKey = testSessionState.getCurrentTestPartKey(); final TestPartSessionState currentTestPartSessionState = testSessionState.getTestPartSessionStates().get(currentTestPartKey); if(currentTestPartSessionState.isEnded()) { fireEvent(ureq, new QTIWorksAssessmentTestEvent(QTIWorksAssessmentTestEvent.Event.advanceTestPart, closeTestButton)); } } } private void doCancelTest(UserRequest ureq) { fireEvent(ureq, Event.CANCELLED_EVENT); } private void doSuspendTest(UserRequest ureq) { fireEvent(ureq, new Event("suspend")); } private void doSaveMenuWidth(UserRequest ureq, String newMenuWidth) { this.menuWidth = newMenuWidth; if(StringHelper.containsNonWhitespace(newMenuWidth)) { flc.contextPut("menuWidth", newMenuWidth); if(testEntry != null) { UserSession usess = ureq.getUserSession(); if (usess.isAuthenticated() && !usess.getRoles().isGuestOnly()) { usess.getGuiPreferences().putAndSave(this.getClass(), getMenuPrefsKey(), newMenuWidth); } } } } private String getMenuPrefsKey() { return "menuWidth_" + testEntry.getKey(); } public boolean isResultsVisible() { return resultsVisible; } /** * Update the status and show the test results the test is at the end * and the configuration allow it. * * @param ureq * @return true if the results are visible */ private boolean updateStatusAndResults(UserRequest ureq) { //updateButtons(); resultsVisible = false; if(testSessionController.getTestSessionState().isEnded() && deliveryOptions.getAssessmentResultsOptions() != null && !deliveryOptions.getAssessmentResultsOptions().none()) { removeAsListenerAndDispose(resultCtrl); resultCtrl = new AssessmentResultController(ureq, getWindowControl(), assessedIdentity, anonym, AssessmentTestDisplayController.this.getCandidateSession(), fUnzippedDirRoot, mapperUri, null, deliveryOptions.getAssessmentResultsOptions(), false, true); listenTo(resultCtrl); flc.add("qtiResults", resultCtrl.getInitialFormItem()); resultsVisible = true; } if(testSessionController.getTestSessionState().isEnded() || testSessionController.findNextEnterableTestPart() == null) { closeTestButton.setI18nKey("assessment.test.close.test"); } else { closeTestButton.setI18nKey("assessment.test.close.testpart"); } closeResultsButton.setVisible(resultsVisible && showCloseResults); updateQtiWorksStatus(); return resultsVisible; } private void updateQtiWorksStatus() { if(deliveryOptions.isDisplayQuestionProgress() || deliveryOptions.isDisplayScoreProgress()) { TestPlanInfos testPlanInfos = new TestPlanInfos(); testSessionController.visitTestPlan(testPlanInfos); if(deliveryOptions.isDisplayQuestionProgress()) { questionProgress.setMax(testPlanInfos.getNumOfItems()); questionProgress.setActual(testPlanInfos.getNumOfAnsweredItems()); qtiWorksStatus.setNumOfItems(testPlanInfos.getNumOfItems()); qtiWorksStatus.setNumOfAnsweredItems(testPlanInfos.getNumOfAnsweredItems()); } if(deliveryOptions.isDisplayScoreProgress()) { double score = testPlanInfos.getScore(); double maxScore = testPlanInfos.getMaxScore(); // real assessment test score overwrite Value assessmentTestScoreValue = testSessionController.getTestSessionState() .getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); if(assessmentTestScoreValue instanceof FloatValue) { score = ((FloatValue)assessmentTestScoreValue).doubleValue(); } Value assessmentTestMaxScoreValue = testSessionController.getTestSessionState() .getOutcomeValue(QTI21Constants.MAXSCORE_IDENTIFIER); if(assessmentTestMaxScoreValue instanceof FloatValue) { maxScore = ((FloatValue)assessmentTestMaxScoreValue).doubleValue(); } qtiWorksStatus.setScore(score); qtiWorksStatus.setMaxScore(maxScore); scoreProgress.setActual((float)score); scoreProgress.setMax((float)maxScore); } } } } public class TestPlanInfos implements TestPlanVisitor { private int numOfItems = 0; private int numOfAnsweredItems = 0; private double score = 0.0d; private double maxScore = 0.0d; @Override public void visit(TestPlanNode testPlanNode) { final TestNodeType type = testPlanNode.getTestNodeType(); if(type == TestNodeType.ASSESSMENT_ITEM_REF) { numOfItems++; ItemSessionState state = testSessionController.getTestSessionState() .getItemSessionStates().get(testPlanNode.getKey()); if(state != null && state.isResponded()) { numOfAnsweredItems++; } Value maxScoreValue = state.getOutcomeValue(QTI21Constants.MAXSCORE_IDENTIFIER); if(maxScoreValue instanceof FloatValue) { maxScore += ((FloatValue)maxScoreValue).doubleValue(); } Value scoreValue = state.getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); if(scoreValue instanceof FloatValue) { score += ((FloatValue)scoreValue).doubleValue(); } } } public int getNumOfItems() { return numOfItems; } public int getNumOfAnsweredItems() { return numOfAnsweredItems; } public double getMaxScore() { return maxScore; } public double getScore() { return score; } } public class QtiWorksStatus { private final DecimalFormat scoreFormat = new DecimalFormat("#0.###", new DecimalFormatSymbols(Locale.ENGLISH)); private int numOfItems; private int numOfAnsweredItems; private double score; private double maxScore; public boolean isSurvey() { return false; } public String getScore() { return scoreFormat.format(score); } public void setScore(double score) { this.score = score; } public boolean hasMaxScore() { return true; } public String getMaxScore() { return scoreFormat.format(maxScore); } public void setMaxScore(double maxScore) { this.maxScore = maxScore; } public String getNumberOfAnsweredQuestions() { return numOfAnsweredItems <= 0 ? "0" : Integer.toString(numOfAnsweredItems); } public void setNumOfItems(int numOfItems) { this.numOfItems = numOfItems; } public String getNumberOfQuestions() { return numOfItems <= 0 ? "0" : Integer.toString(numOfItems); } public void setNumOfAnsweredItems(int numOfAnsweredItems) { this.numOfAnsweredItems = numOfAnsweredItems; } public boolean isAssessmentTestTimeLimit() { Long timeLimits = getAssessmentTestMaxTimeLimit(); return timeLimits != null; } public String getAssessmentTestEndTime() { Long timeLimits = getAssessmentTestMaxTimeLimit(); if(timeLimits != null) { Calendar calendar = Calendar.getInstance(); // gets a calendar using the default time zone and locale. calendar.add(Calendar.SECOND, timeLimits.intValue()); String time = Formatter.getInstance(getLocale()).formatTimeShort(calendar.getTime()); return time; } return ""; } public long getAssessmentTestDuration() { TestSessionState testSessionState = testSessionController.getTestSessionState(); long duration = testSessionState.getDurationAccumulated(); duration += getRequestTimeStampDifferenceToNow(); return duration; } /** * * @return A duration in milliseconds */ public long getAssessmentTestMaximumTimeLimits() { long maxDuration = -1l; Long timeLimits = getAssessmentTestMaxTimeLimit(); if(timeLimits != null) { maxDuration = timeLimits.longValue() * 1000; } return maxDuration; } public boolean isEnded() { TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = AssessmentTestDisplayController.this; return candidateSessionContext.isTerminated() || testSessionState.isExited() || testSessionState.isEnded(); } public boolean maySuspendTest() { TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = AssessmentTestDisplayController.this; return deliveryOptions.isEnableSuspend() && !candidateSessionContext.isTerminated() && !testSessionState.isExited() && !testSessionState.isEnded(); } public boolean mayCancelTest() { TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = AssessmentTestDisplayController.this; return deliveryOptions.isEnableCancel() && !candidateSessionContext.isTerminated() && !testSessionState.isExited() && !testSessionState.isEnded(); } public boolean mayEndCurrentTestPart() { TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = AssessmentTestDisplayController.this; return !candidateSessionContext.isTerminated() && !testSessionState.isExited() && testSessionState.getCurrentTestPartKey() != null && testSessionController.mayEndCurrentTestPart(); } public boolean mayCloseTest() { TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = AssessmentTestDisplayController.this; if(!candidateSessionContext.isTerminated() && !testSessionState.isExited() && testSessionState.getCurrentTestPartKey() != null && testSessionController.mayEndCurrentTestPart()) { final TestPlanNodeKey currentTestPartKey = testSessionState.getCurrentTestPartKey(); final TestPartSessionState currentTestPartSessionState = testSessionState.getTestPartSessionStates().get(currentTestPartKey); if(currentTestPartSessionState.isEnded()) { return true; } } return false; } } }