/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <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> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. */ package org.olat.course.nodes.scorm; import java.io.File; import java.util.Properties; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.Window; import org.olat.core.gui.components.panel.Panel; import org.olat.core.gui.components.tree.TreeEvent; import org.olat.core.gui.components.velocity.VelocityContainer; import org.olat.core.gui.control.ConfigurationChangedListener; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.ControllerEventListener; 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.iframe.DeliveryOptions; import org.olat.core.gui.control.generic.messages.MessageController; import org.olat.core.gui.control.generic.messages.MessageUIFactory; import org.olat.core.gui.util.SyntheticUserRequest; import org.olat.core.logging.AssertException; import org.olat.core.util.CodeHelper; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.UserSession; import org.olat.core.util.Util; import org.olat.core.util.event.GenericEventListener; import org.olat.course.assessment.AssessmentHelper; import org.olat.course.editor.NodeEditController; import org.olat.course.highscore.ui.HighScoreRunController; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.ObjectivesHelper; import org.olat.course.nodes.ScormCourseNode; import org.olat.course.run.scoring.ScoreEvaluation; import org.olat.course.run.userview.UserCourseEnvironment; import org.olat.fileresource.FileResourceManager; import org.olat.instantMessaging.CloseInstantMessagingEvent; import org.olat.instantMessaging.InstantMessagingService; import org.olat.modules.ModuleConfiguration; import org.olat.modules.scorm.ScormAPICallback; import org.olat.modules.scorm.ScormAPIandDisplayController; import org.olat.modules.scorm.ScormCPManifestTreeModel; import org.olat.modules.scorm.ScormConstants; import org.olat.modules.scorm.ScormMainManager; import org.olat.modules.scorm.ScormPackageConfig; import org.olat.repository.RepositoryEntry; import org.olat.util.logging.activity.LoggingResourceable; /** * Description: <BR/> * Run controller for content packaging course nodes * <P/> * * @author Felix Jost */ public class ScormRunController extends BasicController implements ScormAPICallback, GenericEventListener, ConfigurationChangedListener { private ModuleConfiguration config; private File cpRoot; private Panel main; private VelocityContainer startPage; // private Translator translator; private ScormAPIandDisplayController scormDispC; private ScormCourseNode scormNode; // for external menu representation private ScormCPManifestTreeModel treeModel; private ControllerEventListener treeNodeClickListener; private UserCourseEnvironment userCourseEnv; private ChooseScormRunModeForm chooseScormRunMode; private boolean isPreview; private boolean isAssessable; private String assessableType; private DeliveryOptions deliveryOptions; private final UserSession userSession;//need for high score /** * Use this constructor to launch a CP via Repository reference key set in * the ModuleConfiguration. On the into page a title and the learning * objectives can be placed. * * @param config * @param ureq * @param userCourseEnv * @param wControl * @param cpNode */ public ScormRunController(ModuleConfiguration config, UserRequest ureq, UserCourseEnvironment userCourseEnv, WindowControl wControl, ScormCourseNode scormNode, boolean isPreview) { super(ureq, wControl, Util.createPackageTranslator(CourseNode.class, ureq.getLocale())); // assertion to make sure the moduleconfig is valid if (!ScormEditController.isModuleConfigValid(config)) throw new AssertException("scorm run controller had an invalid module config:" + config.toString()); this.isPreview = isPreview || userCourseEnv.isCourseReadOnly(); this.userCourseEnv = userCourseEnv; this.config = config; this.scormNode = scormNode; userSession = ureq.getUserSession(); deliveryOptions = (DeliveryOptions)config.get(ScormEditController.CONFIG_DELIVERY_OPTIONS); addLoggingResourceable(LoggingResourceable.wrap(scormNode)); init(ureq); } private void init(UserRequest ureq) { startPage = createVelocityContainer("run"); // show browse mode option only if not assessable, hide it if in // "real test mode" isAssessable = config.getBooleanSafe(ScormEditController.CONFIG_ISASSESSABLE, true); if(isAssessable) { assessableType = config.getStringValue(ScormEditController.CONFIG_ASSESSABLE_TYPE, ScormEditController.CONFIG_ASSESSABLE_TYPE_SCORE); } // <OLATCE-289> // attemptsDependOnScore means that attempts are only incremented when a // score was given back by the SCORM // set start button if max attempts are not reached if (!maxAttemptsReached()) { chooseScormRunMode = new ChooseScormRunModeForm(ureq, getWindowControl(), !isAssessable, userCourseEnv.isCourseReadOnly()); listenTo(chooseScormRunMode); startPage.put("chooseScormRunMode", chooseScormRunMode.getInitialComponent()); startPage.contextPut("maxAttemptsReached", Boolean.FALSE); } else { startPage.contextPut("maxAttemptsReached", Boolean.TRUE); } // </OLATCE-289> main = new Panel("scormrunmain"); doStartPage(ureq); putInitialPanel(main); boolean doSkip = config.getBooleanSafe(ScormEditController.CONFIG_SKIPLAUNCHPAGE, false); if (doSkip && !maxAttemptsReached()) { doLaunch(ureq, true); getWindowControl().getWindowBackOffice().addCycleListener(this); } } // <OLATCE-289> /** * @return true if attempts of the user are equal to the maximum number of * attempts. */ private boolean maxAttemptsReached() { int maxAttempts = config.getIntegerSafe(ScormEditController.CONFIG_MAXATTEMPTS, 0); boolean maxAttemptsReached = false; if (maxAttempts > 0) { if (scormNode.getUserAttempts(userCourseEnv) >= maxAttempts) { maxAttemptsReached = true; } } return maxAttemptsReached; } // </OLATCE-289> /** * @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest, * org.olat.core.gui.components.Component, * org.olat.core.gui.control.Event) */ public void event(UserRequest ureq, Component source, Event event) { // } /** * @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 ureq, Controller source, Event event) { if (source == scormDispC) { // just pass on the event. // <OLATCE-289> if (event.equals(Event.BACK_EVENT)) { if (maxAttemptsReached()) { startPage.contextPut("maxAttemptsReached", Boolean.TRUE); } doStartPage(ureq); } else { // </OLATCE-289> doStartPage(ureq); fireEvent(ureq, event); } } else if (source == null) { // external source if (event instanceof TreeEvent) { scormDispC.switchToPage((TreeEvent) event); } } else if (source == chooseScormRunMode) { doLaunch(ureq, true); if(scormDispC != null) { scormDispC.activate(); } } } private void doStartPage(UserRequest ureq) { // push title and learning objectives, only visible on intro page startPage.contextPut("menuTitle", scormNode.getShortTitle()); startPage.contextPut("displayTitle", scormNode.getLongTitle()); // Adding learning objectives String learningObj = scormNode.getLearningObjectives(); if (learningObj != null) { Component learningObjectives = ObjectivesHelper.createLearningObjectivesComponent(learningObj, getLocale()); startPage.put("learningObjectives", learningObjectives); startPage.contextPut("hasObjectives", Boolean.TRUE); } else { startPage.contextPut("hasObjectives", Boolean.FALSE); } if (isAssessable) { ScoreEvaluation scoreEval = scormNode.getUserScoreEvaluation(userCourseEnv); Float score = scoreEval.getScore(); if(ScormEditController.CONFIG_ASSESSABLE_TYPE_SCORE.equals(assessableType)) { startPage.contextPut("score", score != null ? AssessmentHelper.getRoundedScore(score) : "0"); } startPage.contextPut("hasPassedValue", (scoreEval.getPassed() == null ? Boolean.FALSE : Boolean.TRUE)); startPage.contextPut("passed", scoreEval.getPassed()); boolean resultsVisible = scoreEval.getUserVisible() == null || scoreEval.getUserVisible().booleanValue(); startPage.contextPut("resultsVisible", resultsVisible); if(resultsVisible && scormNode.hasCommentConfigured()) { StringBuilder comment = Formatter.stripTabsAndReturns(scormNode.getUserUserComment(userCourseEnv)); startPage.contextPut("comment", StringHelper.xssScan(comment)); } startPage.contextPut("attempts", scormNode.getUserAttempts(userCourseEnv)); if(ureq == null) {// High score need one ureq = new SyntheticUserRequest(getIdentity(), getLocale(), userSession); } HighScoreRunController highScoreCtr = new HighScoreRunController(ureq, getWindowControl(), userCourseEnv, scormNode); if (highScoreCtr.isViewHighscore()) { Component highScoreComponent = highScoreCtr.getInitialComponent(); startPage.put("highScore", highScoreComponent); } } startPage.contextPut("isassessable", Boolean.valueOf(isAssessable)); main.setContent(startPage); } private void doSetMissingResourcesWarning(UserRequest ureq) { String text = translate("error.cprepoentrymissing.user"); MessageController missingCtrl = MessageUIFactory.createWarnMessage(ureq, getWindowControl(), null, text); listenTo(missingCtrl); main.setContent(missingCtrl.getInitialComponent()); } private void doLaunch(UserRequest ureq, boolean doActivate) { ureq.getUserSession().getSingleUserEventCenter() .fireEventToListenersOf(new CloseInstantMessagingEvent(), InstantMessagingService.TOWER_EVENT_ORES); if (cpRoot == null) { // it is the first time we start the contentpackaging from this // instance // of this controller. // need to be strict when launching -> "true" RepositoryEntry re = ScormEditController.getScormCPReference(config, false); if (re == null) { doSetMissingResourcesWarning(ureq); return; } cpRoot = FileResourceManager.getInstance().unzipFileResource(re.getOlatResource()); addLoggingResourceable(LoggingResourceable.wrapScormRepositoryEntry(re)); // should always exist because references cannot be deleted as long // as // nodes reference them if (cpRoot == null) { doSetMissingResourcesWarning(ureq); logError("File of repository entry " + re.getKey() + " was missing", null); return; } } // else cpRoot is already set (save some db access if the user opens / // closes / reopens the cp from the same CPRuncontroller instance) String courseId; boolean showMenu = config.getBooleanSafe(ScormEditController.CONFIG_SHOWMENU, true); final boolean fullWindow = config.getBooleanSafe(ScormEditController.CONFIG_FULLWINDOW, true); if (isPreview) { courseId = new Long(CodeHelper.getRAMUniqueID()).toString(); scormDispC = ScormMainManager.getInstance().createScormAPIandDisplayController(ureq, getWindowControl(), showMenu, null, cpRoot, null, courseId, ScormConstants.SCORM_MODE_BROWSE, ScormConstants.SCORM_MODE_NOCREDIT, true, null, doActivate, fullWindow, false, deliveryOptions); } else { boolean attemptsIncremented = false; //increment user attempts only once! if(!config.getBooleanSafe(ScormEditController.CONFIG_ADVANCESCORE, true) || !config.getBooleanSafe(ScormEditController.CONFIG_ATTEMPTSDEPENDONSCORE, false)) { scormNode.incrementUserAttempts(userCourseEnv); attemptsIncremented = true; } courseId = userCourseEnv.getCourseEnvironment().getCourseResourceableId().toString(); if (isAssessable) { // When a SCORE is transfered, the run mode is hardcoded scormDispC = ScormMainManager.getInstance().createScormAPIandDisplayController(ureq, getWindowControl(), showMenu, this, cpRoot, null, courseId + "-" + scormNode.getIdent(), ScormConstants.SCORM_MODE_NORMAL, ScormConstants.SCORM_MODE_CREDIT, false, assessableType, doActivate, fullWindow, attemptsIncremented, deliveryOptions); } else if (chooseScormRunMode.getSelectedElement().equals(ScormConstants.SCORM_MODE_NORMAL)) { // When not assessible users can choose between normal mode where data is stored... scormDispC = ScormMainManager.getInstance().createScormAPIandDisplayController(ureq, getWindowControl(), showMenu, this, cpRoot, null, courseId + "-" + scormNode.getIdent(), ScormConstants.SCORM_MODE_NORMAL, ScormConstants.SCORM_MODE_CREDIT, false, assessableType, doActivate, fullWindow, attemptsIncremented, deliveryOptions); } else { // ... and preview mode where no data is stored scormDispC = ScormMainManager.getInstance().createScormAPIandDisplayController(ureq, getWindowControl(), showMenu, this, cpRoot, null, courseId, ScormConstants.SCORM_MODE_BROWSE, ScormConstants.SCORM_MODE_NOCREDIT, false, assessableType, doActivate, fullWindow, attemptsIncremented, deliveryOptions); } } // configure some display options boolean showNavButtons = config.getBooleanSafe(ScormEditController.CONFIG_SHOWNAVBUTTONS, true); scormDispC.showNavButtons(showNavButtons); if(deliveryOptions != null && deliveryOptions.getInherit() != null && deliveryOptions.getInherit().booleanValue()) { ScormPackageConfig pConfig = ScormMainManager.getInstance().getScormPackageConfig(cpRoot); deliveryOptions = (pConfig == null ? null : pConfig.getDeliveryOptions()); } if(deliveryOptions == null) { scormDispC.setHeightPX(680); } else { scormDispC.setDeliveryOptions(deliveryOptions); } listenTo(scormDispC); // the scormDispC activates itself } /* * (non-Javadoc) * * @see org.olat.modules.scorm.ScormAPICallback#lmsCommit(java.lang.String, * java.util.Properties) */ @Override public void lmsCommit(String olatSahsId, Properties scoreProp, Properties lessonStatusProp) { // } /** * @see org.olat.modules.scorm.ScormAPICallback#lmsFinish(java.lang.String, * java.util.Properties) */ @Override public void lmsFinish(String olatSahsId, Properties scoreProp, Properties lessonStatusProp) { if (config.getBooleanSafe(ScormEditController.CONFIG_CLOSE_ON_FINISH, false)) { doStartPage(null); scormDispC.close(); } } /** * @return true if there is a treemodel and an event listener ready to be * used in outside this controller */ public boolean isExternalMenuConfigured() { return (config.getBooleanEntry(NodeEditController.CONFIG_COMPONENT_MENU).booleanValue()); } @Override public void configurationChanged() { if(scormDispC != null) { scormDispC.configurationChanged(); } } /** * @see org.olat.core.gui.control.DefaultController#doDispose(boolean) */ @Override protected void doDispose() { // } /** * @return the treemodel of the enclosed ScormDisplayController, or null, if * no tree should be displayed (configured by author, see * DisplayConfigurationForm.CONFIG_COMPONENT_MENU) */ public ScormCPManifestTreeModel getTreeModel() { return treeModel; } /** * @return the listener to listen to clicks to the nodes of the treemodel * obtained calling getTreeModel() */ public ControllerEventListener getTreeNodeClickListener() { return treeNodeClickListener; } @Override public void event(Event event) { if (event == Window.END_OF_DISPATCH_CYCLE || event == Window.BEFORE_RENDER_ONLY) { // do initial modal dialog activation // a) just after the dispatching of the event which is before // rendering after a normal click // b) just before a render-only operation which happens when using a // jump-in URL followed by a redirect without dispatching scormDispC.activate(); getWindowControl().getWindowBackOffice().removeCycleListener(this); } } }