/** * 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.run; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import org.olat.core.commons.fullWebApp.LayoutMain3ColsController; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.htmlsite.OlatCmdEvent; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.link.LinkFactory; import org.olat.core.gui.components.panel.Panel; import org.olat.core.gui.components.stack.TooledStackedPanel; import org.olat.core.gui.components.stack.TooledStackedPanel.Align; import org.olat.core.gui.components.tree.GenericTreeModel; import org.olat.core.gui.components.tree.MenuTree; import org.olat.core.gui.components.tree.TreeEvent; import org.olat.core.gui.components.tree.TreeModel; import org.olat.core.gui.components.tree.TreeNode; 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.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.MainLayoutBasicController; import org.olat.core.gui.control.generic.dtabs.Activateable2; import org.olat.core.gui.control.generic.messages.MessageController; import org.olat.core.gui.control.generic.messages.MessageUIFactory; import org.olat.core.gui.control.generic.textmarker.GlossaryMarkupItemController; import org.olat.core.gui.control.generic.title.TitledWrapperController; import org.olat.core.gui.control.winmgr.JSCommand; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.id.context.ContextEntry; import org.olat.core.id.context.StateEntry; import org.olat.core.logging.activity.CourseLoggingAction; import org.olat.core.logging.activity.ThreadLocalUserActivityLogger; import org.olat.core.util.Formatter; import org.olat.core.util.Util; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.prefs.Preferences; import org.olat.core.util.resource.OLATResourceableJustBeforeDeletedEvent; import org.olat.core.util.resource.OresHelper; import org.olat.core.util.tree.TreeHelper; import org.olat.course.CourseFactory; import org.olat.course.CourseModule; import org.olat.course.DisposedCourseRestartController; import org.olat.course.ICourse; import org.olat.course.assessment.AssessmentChangedEvent; import org.olat.course.assessment.AssessmentMode; import org.olat.course.assessment.AssessmentMode.Status; import org.olat.course.config.CourseConfig; import org.olat.course.editor.PublishEvent; import org.olat.course.groupsandrights.CourseGroupManager; import org.olat.course.nodes.CourseNode; import org.olat.course.run.glossary.CourseGlossaryFactory; import org.olat.course.run.glossary.CourseGlossaryToolLinkController; import org.olat.course.run.navigation.NavigationHandler; import org.olat.course.run.navigation.NodeClickedRef; import org.olat.course.run.userview.AssessmentModeTreeFilter; import org.olat.course.run.userview.InvisibleTreeFilter; import org.olat.course.run.userview.TreeFilter; import org.olat.course.run.userview.UserCourseEnvironmentImpl; import org.olat.course.run.userview.VisibleTreeFilter; import org.olat.group.BusinessGroup; import org.olat.group.ui.edit.BusinessGroupModifiedEvent; import org.olat.modules.cp.TreeNodeEvent; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryManager; import org.olat.repository.RepositoryService; import org.olat.repository.model.RepositoryEntrySecurity; import org.olat.util.logging.activity.LoggingResourceable; import org.springframework.beans.factory.annotation.Autowired; /** * Description: <br> * * @author Felix Jost */ public class RunMainController extends MainLayoutBasicController implements GenericEventListener, Activateable2 { public static final String REBUILD = "rebuild"; public static final String ORES_TYPE_COURSE_RUN = OresHelper.calculateTypeName(RunMainController.class, CourseModule.ORES_TYPE_COURSE); private final OLATResourceable courseRunOres; //course run ores for course run channel private ICourse course;//o_clusterOK: this controller listen to course change events private RepositoryEntry courseRepositoryEntry; private Panel contentP; private MenuTree luTree; private VelocityContainer coursemain; private NavigationHandler navHandler; private UserCourseEnvironmentImpl uce; private TooledStackedPanel toolbarPanel; private LayoutMain3ColsController columnLayoutCtr; private Controller currentNodeController; // the currently open node config private boolean isInEditor = false; private CourseNode currentCourseNode; private TreeModel treeModel; private TreeFilter treeFilter; private boolean needsRebuildAfter = false; private boolean needsRebuildAfterPublish = false; private boolean needsRebuildAfterRunDone = false; private boolean assessmentChangedEventReceived = false; private String courseTitle; private Link nextLink, previousLink; private GlossaryMarkupItemController glossaryMarkerCtr; @Autowired private RepositoryManager repositoryManager; @Autowired private RepositoryService repositoryService; /** * Constructor for the run main controller * * @param ureq The user request * @param wControl The current window controller * @param course The current course * @param initialViewIdentifier if null the default view will be started, * otherwise a controllerfactory type dependant view will be * activated (subscription subtype) number: node with nodenumber will * be activated "assessmentTool": assessment tool will be activated * @param offerBookmark - whether to offer bookmarks or not * @param showCourseConfigLink Flag to enable/disable link to detail-page in tool menu. */ public RunMainController(UserRequest ureq, WindowControl wControl, TooledStackedPanel toolbarPanel, ICourse course, RepositoryEntry re, RepositoryEntrySecurity reSecurity, AssessmentMode assessmentMode) { // Use repository package as fallback translator super(ureq, wControl, Util.createPackageTranslator(RepositoryEntry.class, ureq.getLocale())); this.course = course; this.toolbarPanel = toolbarPanel; addLoggingResourceable(LoggingResourceable.wrap(course)); this.courseTitle = course.getCourseTitle(); this.courseRepositoryEntry = re; this.courseRunOres = OresHelper.createOLATResourceableInstance(ORES_TYPE_COURSE_RUN, course.getResourceableId()); Identity identity = ureq.getIdentity(); // course and system logging ThreadLocalUserActivityLogger.log(CourseLoggingAction.COURSE_ENTERING, getClass()); // log shows who entered which course, this can then be further used to jump // to the courselog logAudit("Entering course: [[["+courseTitle+"]]]", course.getResourceableId().toString()); luTree = new MenuTree(null, "luTreeRun", this); luTree.setScrollTopOnClick(true); luTree.setExpandSelectedNode(false); luTree.setElementCssClass("o_course_menu"); contentP = new Panel("building_block_content"); // build up the running structure for this user; // get all group memberships for this course uce = loadUserCourseEnvironment(ureq, reSecurity); // build score now uce.getScoreAccounting().evaluateAll(); if(assessmentMode != null && assessmentMode.isRestrictAccessElements()) { Status assessmentStatus = assessmentMode.getStatus(); if(assessmentStatus == Status.assessment) { treeFilter = new AssessmentModeTreeFilter(assessmentMode, uce.getCourseEnvironment().getRunStructure()); } else if(assessmentStatus == Status.leadtime || assessmentStatus == Status.followup) { treeFilter = new InvisibleTreeFilter(); } else { treeFilter = new VisibleTreeFilter(); } } else { treeFilter = new VisibleTreeFilter(); } navHandler = new NavigationHandler(uce, treeFilter, false); currentCourseNode = updateTreeAndContent(ureq, currentCourseNode, null); if (courseRepositoryEntry != null && repositoryManager.createRepositoryEntryStatus(courseRepositoryEntry.getStatusCode()).isClosed()) { wControl.setWarning(translate("course.closed")); } // add text marker wrapper controller to implement course glossary // textMarkerCtr must be created before the toolC! CourseConfig cc = uce.getCourseEnvironment().getCourseConfig(); glossaryMarkerCtr = CourseGlossaryFactory.createGlossaryMarkupWrapper(ureq, wControl, contentP, cc); MenuTree layoutTree = luTree; if(!cc.isMenuEnabled() && !uce.isAdmin()) { layoutTree = null; } if (glossaryMarkerCtr != null) { listenTo(glossaryMarkerCtr); // enable / disable glossary highlighting according to user prefs Preferences prefs = ureq.getUserSession().getGuiPreferences(); Boolean state = (Boolean) prefs.get(CourseGlossaryToolLinkController.class, CourseGlossaryFactory.createGuiPrefsKey(course)); //Glossary always on for guests. OLAT-4241 if(ureq.getUserSession().getRoles().isGuestOnly()){ state = Boolean.TRUE; } if (state == null) { glossaryMarkerCtr.setTextMarkingEnabled(false); } else { glossaryMarkerCtr.setTextMarkingEnabled(state.booleanValue()); } columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), layoutTree, glossaryMarkerCtr.getInitialComponent(), "course" + course.getResourceableId()); } else { columnLayoutCtr = new LayoutMain3ColsController(ureq, getWindowControl(), layoutTree, contentP, "courseRun" + course.getResourceableId()); } listenTo(columnLayoutCtr); // activate the custom course css if any setCustomCSS(CourseFactory.getCustomCourseCss(ureq.getUserSession(), uce.getCourseEnvironment())); coursemain = createVelocityContainer("index"); coursemain.setDomReplaceable(false); // see function gotonode in functions.js to see why we need the repositoryentry-key here: // it is to correctly apply external links using course-internal links via javascript coursemain.contextPut("courserepokey", courseRepositoryEntry.getKey()); coursemain.put("coursemain", columnLayoutCtr.getInitialComponent()); // on initial call we have to set the data-nodeid manually. later it // will be updated by updateCourseDataAttributes() automatically, but // only when course visible to users (menu tree not null) if (treeModel != null) { String initNodeId = currentCourseNode != null ? currentCourseNode.getIdent() : null; if (initNodeId == null) { initNodeId = treeModel.getRootNode().getIdent(); } coursemain.contextPut("initNodeId", initNodeId); } putInitialPanel(coursemain); // disposed message controller must be created beforehand Controller courseCloser = new DisposedCourseRestartController(ureq, getWindowControl(), courseRepositoryEntry); Controller disposedRestartController = new LayoutMain3ColsController(ureq, wControl, courseCloser); setDisposedMsgController(disposedRestartController); // add as listener to course so we are being notified about course events: // - publish changes // - assessment events // - listen for CourseConfig events CoordinatorManager.getInstance().getCoordinator().getEventBus().registerFor(this, identity, course); // - group modification events CoordinatorManager.getInstance().getCoordinator().getEventBus().registerFor(this, identity, courseRepositoryEntry); } protected void setTextMarkingEnabled(boolean enabled) { if (glossaryMarkerCtr != null) { glossaryMarkerCtr.setTextMarkingEnabled(enabled); } } protected void initToolbar() { if(toolbarPanel != null) { // new toolbox 'general' previousLink = LinkFactory.createToolLink("previouselement","", this, "o_icon_previous_toolbar"); previousLink.setTitle(translate("command.previous")); toolbarPanel.addTool(previousLink, Align.rightEdge, false, "o_tool_previous"); nextLink = LinkFactory.createToolLink("nextelement","", this, "o_icon_next_toolbar"); nextLink.setTitle(translate("command.next")); toolbarPanel.addTool(nextLink, Align.rightEdge, false, "o_tool_next"); updateNextPrevious(); } } private UserCourseEnvironmentImpl loadUserCourseEnvironment(UserRequest ureq, RepositoryEntrySecurity reSecurity) { CourseGroupManager cgm = course.getCourseEnvironment().getCourseGroupManager(); List<BusinessGroup> coachedGroups; if(reSecurity.isGroupCoach()) { coachedGroups = cgm.getOwnedBusinessGroups(ureq.getIdentity()); } else { coachedGroups = Collections.emptyList(); } List<BusinessGroup> participatedGroups; if(reSecurity.isGroupParticipant()) { participatedGroups = cgm.getParticipatingBusinessGroups(ureq.getIdentity()); } else { participatedGroups = Collections.emptyList(); } List<BusinessGroup> waitingLists; if(reSecurity.isGroupWaiting()) { waitingLists = cgm.getWaitingListGroups(ureq.getIdentity()); } else { waitingLists = Collections.emptyList(); } return new UserCourseEnvironmentImpl(ureq.getUserSession().getIdentityEnvironment(), course.getCourseEnvironment(), getWindowControl(), coachedGroups, participatedGroups, waitingLists, reSecurity.isCourseCoach() || reSecurity.isGroupCoach(), reSecurity.isEntryAdmin(), reSecurity.isCourseParticipant() || reSecurity.isGroupParticipant(), reSecurity.isReadOnly()); } /** * Initialize the users group memberships for groups used within this course * * @param identity */ protected void reloadGroupMemberships(RepositoryEntrySecurity reSecurity) { CourseGroupManager cgm = course.getCourseEnvironment().getCourseGroupManager(); List<BusinessGroup> coachedGroups; if(reSecurity.isGroupCoach()) { coachedGroups = cgm.getOwnedBusinessGroups(getIdentity()); } else { coachedGroups = Collections.emptyList(); } List<BusinessGroup> participatedGroups; if(reSecurity.isGroupParticipant()) { participatedGroups = cgm.getParticipatingBusinessGroups(getIdentity()); } else { participatedGroups = Collections.emptyList(); } List<BusinessGroup> waitingLists; if(reSecurity.isGroupWaiting()) { waitingLists = cgm.getWaitingListGroups(getIdentity()); } else { waitingLists = Collections.emptyList(); } uce.setCourseReadOnly(reSecurity.isReadOnly()); uce.setGroupMemberships(coachedGroups, participatedGroups, waitingLists); needsRebuildAfterRunDone = true; } private CourseNode updateAfterChanges(CourseNode courseNode) { if(currentCourseNode == null) return null; CourseNode newCurrentCourseNode; NodeClickedRef nclr = navHandler.reloadTreeAfterChanges(courseNode); if(nclr == null) { doDisposeAfterEvent(); newCurrentCourseNode = null; } else { treeModel = nclr.getTreeModel(); luTree.setTreeModel(treeModel); String selNodeId = nclr.getSelectedNodeId(); luTree.setSelectedNodeId(selNodeId); luTree.setOpenNodeIds(nclr.getOpenNodeIds()); newCurrentCourseNode = nclr.getCalledCourseNode(); } return newCurrentCourseNode; } protected void updateNextPrevious() { if(nextLink == null || previousLink == null || luTree == null) { return; } boolean hasPrevious; boolean hasNext; if(luTree.getSelectedNode() == null) { hasPrevious = true; hasNext = true; } else { List<TreeNode> flatTree = new ArrayList<>(); TreeHelper.makeTreeFlat(luTree.getTreeModel().getRootNode(), flatTree); int index = flatTree.indexOf(luTree.getSelectedNode()); hasPrevious = index > 0; hasNext = index >= 0 && index+1 < flatTree.size(); } previousLink.setEnabled(hasPrevious); nextLink.setEnabled(hasNext); } protected CourseNode updateCurrentCourseNode(UserRequest ureq) { return updateTreeAndContent(ureq, getCurrentCourseNode(), "", null, null); } /** * side-effecty to content and luTree * * @param ureq * @param calledCourseNode the node to jump to, if null = jump to root node * @param nodecmd An optional command used to activate the run view or NULL if not available * @return true if the node jumped to is visible */ private CourseNode updateTreeAndContent(UserRequest ureq, CourseNode calledCourseNode, String nodecmd) { return updateTreeAndContent(ureq, calledCourseNode, nodecmd, null, null); } private CourseNode updateTreeAndContent(UserRequest ureq, CourseNode calledCourseNode, String nodecmd, List<ContextEntry> entries, StateEntry state) { // build menu (treemodel) // dispose old node controller before creating the NodeClickedRef which creates // the new node controller. It is important that the old node controller is // disposed before the new one to not get conflicts with cacheable mappers that // might be used in both controllers with the same ID (e.g. the course folder) if (currentNodeController != null && !currentNodeController.isDisposed() && !navHandler.isListening(currentNodeController)) { currentNodeController.dispose(); } NodeClickedRef nclr = navHandler.evaluateJumpToCourseNode(ureq, getWindowControl(), calledCourseNode, this, nodecmd); if (!nclr.isVisible()) { // if not root -> fallback to root. e.g. when a direct node jump fails if (calledCourseNode != null) { nclr = navHandler.evaluateJumpToCourseNode(ureq, getWindowControl(), null, this, null); } if (!nclr.isVisible()) { MessageController msgController = MessageUIFactory.createInfoMessage(ureq, getWindowControl(), translate("course.noaccess.title"), translate("course.noaccess.text")); contentP.setContent(msgController.getInitialComponent()); luTree.setTreeModel(new GenericTreeModel()); return null; } } treeModel = nclr.getTreeModel(); luTree.setTreeModel(treeModel); String selNodeId = nclr.getSelectedNodeId(); luTree.setSelectedNodeId(selNodeId); luTree.setOpenNodeIds(nclr.getOpenNodeIds()); // get new run controller. currentNodeController = nclr.getRunController(); addToHistory(ureq, currentNodeController); if (currentNodeController instanceof TitledWrapperController) { Controller contentcontroller = ((TitledWrapperController)currentNodeController).getContentController(); addToHistory(ureq, contentcontroller); if(contentcontroller instanceof Activateable2) { ((Activateable2)contentcontroller).activate(ureq, entries, state); } } else if(currentNodeController instanceof Activateable2) { ((Activateable2)currentNodeController).activate(ureq, entries, state); } if(currentNodeController != null) { contentP.setContent(currentNodeController.getInitialComponent()); } else { MessageController msgCtrl = MessageUIFactory .createWarnMessage(ureq, getWindowControl(), null, translate("msg.nodenotavailableanymore")); listenTo(msgCtrl); contentP.setContent(msgCtrl.getInitialComponent()); } updateNextPrevious(); updateCourseDataAttributes(nclr.getCalledCourseNode()); updateLastUsage(nclr.getCalledCourseNode()); return nclr.getCalledCourseNode(); } private void updateLastUsage(CourseNode calledCourseNode) { if(calledCourseNode != null && calledCourseNode.needsReferenceToARepositoryEntry()) { RepositoryEntry referencedRe = calledCourseNode.getReferencedRepositoryEntry(); if(referencedRe != null) { repositoryService.setLastUsageNowFor(referencedRe); } } } private void updateCourseDataAttributes(CourseNode calledCourseNode) { StringBuilder sb = new StringBuilder(); sb.append("try {var oocourse = jQuery('.o_course_run');"); if (calledCourseNode == null) { sb.append("oocourse.removeAttr('data-nodeid');"); } else { sb.append("oocourse.attr('data-nodeid','"); sb.append(Formatter.escapeDoubleQuotes(calledCourseNode.getIdent())); sb.append("');"); } sb.append("oocourse=null;}catch(e){}"); JSCommand jsc = new JSCommand(sb.toString()); WindowControl wControl = getWindowControl(); if (wControl != null && wControl.getWindowBackOffice() != null) { wControl.getWindowBackOffice().sendCommandTo(jsc); } } /** * @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) */ @Override public void event(UserRequest ureq, Component source, Event event) { if(needsRebuildAfter) { currentCourseNode = updateAfterChanges(currentCourseNode); needsRebuildAfter = false; } // Links in editTools dropdown if(nextLink == source) { doNext(ureq); } else if(previousLink == source) { doPrevious(ureq); } else if (source == luTree) { if (event.getCommand().equals(MenuTree.COMMAND_TREENODE_CLICKED) || event.getCommand().equals(MenuTree.COMMAND_TREENODE_EXPANDED)) { TreeEvent tev = (TreeEvent) event; doNodeClick(ureq, tev); } } else if (source == coursemain) { if ("activateCourseNode".equals(event.getCommand())) { // Events from the JS function o_activateCourseNode() - activate the given node id String nodeid = ureq.getParameter("nodeid"); if (nodeid != null) { CourseNode identNode = course.getRunStructure().getNode(nodeid); CourseNode node = updateTreeAndContent(ureq, identNode, null); if (node != null) { currentCourseNode = node; } else { getWindowControl().setWarning(translate("msg.nodenotavailableanymore")); } } } } } protected void toolCtrDone(UserRequest ureq, RepositoryEntrySecurity reSecurity) { if (isInEditor) { isInEditor = false; // for clarity if (needsRebuildAfterPublish) { needsRebuildAfterPublish = false; // rebuild up the running structure for this user, after publish; course = CourseFactory.loadCourse(course.getResourceableId()); uce = loadUserCourseEnvironment(ureq, reSecurity); // build score now uce.getScoreAccounting().evaluateAll(); navHandler = new NavigationHandler(uce, treeFilter, false); // rebuild and jump to root node updateTreeAndContent(ureq, null, null); } } } protected boolean isInEditor() { return isInEditor; } protected void setInEditor(boolean isInEditor) { this.isInEditor = isInEditor; } /** * @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) */ @Override public void event(UserRequest ureq, Controller source, Event event) { if(needsRebuildAfter) { currentCourseNode = updateAfterChanges(currentCourseNode); needsRebuildAfter = false; } // event from the current tool (editor, groupmanagement, archiver) if (source == currentNodeController) { if (event instanceof OlatCmdEvent) { OlatCmdEvent oe = (OlatCmdEvent) event; String cmd = oe.getCommand(); if (OlatCmdEvent.GOTONODE_CMD.equals(cmd)) { String subcmd = oe.getSubcommand(); // "69680861018558/node-specific-data"; CourseNode identNode; String nodecmd = null; int slPos = subcmd.indexOf('/'); if (slPos != -1) { nodecmd = subcmd.substring(slPos + 1); identNode = course.getRunStructure().getNode(subcmd.substring(0, slPos)); } else { identNode = course.getRunStructure().getNode(subcmd); } if(identNode == null) { showWarning("msg.nodenotavailableanymore"); } else { addLoggingResourceable(LoggingResourceable.wrap(identNode)); currentCourseNode = identNode; updateTreeAndContent(ureq, identNode, nodecmd); oe.accept(); } } } else if (event == Event.DONE_EVENT) { // the controller is done. // we have a chance here to test if we need to refresh the evalution. // this is the case when a test was submitted and scoring has changed -> // preconditions may have changed if (needsRebuildAfterRunDone) { needsRebuildAfterRunDone = false; updateTreeAndContent(ureq, currentCourseNode, null); } } else if (REBUILD.equals(event.getCommand())) { needsRebuildAfterRunDone = false; updateTreeAndContent(ureq, currentCourseNode, null); } else if (event instanceof TreeNodeEvent) { TreeNodeEvent tne = (TreeNodeEvent) event; TreeNode newCpTreeNode = tne.getChosenTreeNode(); luTree.setSelectedNodeId(newCpTreeNode.getIdent()); } else if (event == Event.CHANGED_EVENT) { updateTreeAndContent(ureq, currentCourseNode, null); } else if (event instanceof BusinessGroupModifiedEvent) { fireEvent(ureq, event); updateTreeAndContent(ureq, currentCourseNode, null); } } } private void doNext(UserRequest ureq) { List<TreeNode> flatList = new ArrayList<>(); TreeNode currentNode = luTree.getSelectedNode(); TreeHelper.makeTreeFlat(luTree.getTreeModel().getRootNode(), flatList); int index = flatList.indexOf(currentNode); if(index >= 0 && index+1 <flatList.size()) { TreeNode nextNode = flatList.get(index + 1); TreeEvent tev = new TreeEvent(MenuTree.COMMAND_TREENODE_CLICKED, nextNode.getIdent()); doNodeClick(ureq, tev); } } private void doPrevious(UserRequest ureq) { List<TreeNode> flatList = new ArrayList<>(); TreeNode currentNode = luTree.getSelectedNode(); TreeHelper.makeTreeFlat(luTree.getTreeModel().getRootNode(), flatList); int index = flatList.indexOf(currentNode); if(index-1 >= 0 && index-1 < flatList.size()) { TreeNode previousNode = flatList.get(index - 1); TreeEvent tev = new TreeEvent(MenuTree.COMMAND_TREENODE_CLICKED, previousNode.getIdent()); doNodeClick(ureq, tev); } } private void doNodeClick(UserRequest ureq, TreeEvent tev) { if(assessmentChangedEventReceived) { uce.getScoreAccounting().evaluateAll(); assessmentChangedEventReceived = false; } // goto node: // after a click in the tree, evaluate the model anew, and set the // selection of the tree again NodeClickedRef nclr = navHandler.evaluateJumpToTreeNode(ureq, getWindowControl(), treeModel, tev, this, null, currentNodeController); if (!nclr.isVisible()) { getWindowControl().setWarning(translate("msg.nodenotavailableanymore")); // go to root since the current node is no more visible updateTreeAndContent(ureq, null, null); updateNextPrevious(); updateCourseDataAttributes(nclr.getCalledCourseNode()); return; } // a click to a subtree's node if (nclr.isHandledBySubTreeModelListener() || nclr.getSelectedNodeId() == null) { if(nclr.getRunController() != null) { //there is an update to the currentNodeController, apply it if (currentNodeController != null && !currentNodeController.isDisposed() && !navHandler.isListening(currentNodeController)) { currentNodeController.dispose(); } currentNodeController = nclr.getRunController(); Component nodeComp = currentNodeController.getInitialComponent(); contentP.setContent(nodeComp); updateLastUsage(nclr.getCalledCourseNode()); } if(nclr.getSelectedNodeId() != null && nclr.getOpenNodeIds() != null) { luTree.setSelectedNodeId(nclr.getSelectedNodeId()); luTree.setOpenNodeIds(nclr.getOpenNodeIds()); } updateNextPrevious(); updateCourseDataAttributes(nclr.getCalledCourseNode()); return; } // set the new treemodel treeModel = nclr.getTreeModel(); luTree.setTreeModel(treeModel); // set the new tree selection String nodeId = nclr.getSelectedNodeId(); luTree.setSelectedNodeId(nodeId); luTree.setOpenNodeIds(nclr.getOpenNodeIds()); currentCourseNode = nclr.getCalledCourseNode(); // get new run controller. Dispose only if not already disposed in navHandler.evaluateJumpToTreeNode() if (currentNodeController != null && !currentNodeController.isDisposed() && !navHandler.isListening(currentNodeController)) { currentNodeController.dispose(); } currentNodeController = nclr.getRunController(); updateLastUsage(nclr.getCalledCourseNode()); Component nodeComp = currentNodeController.getInitialComponent(); contentP.setContent(nodeComp); addToHistory(ureq, currentNodeController); // set glossary wrapper dirty after menu click to make it reload the glossary // stuff properly when in AJAX mode if (glossaryMarkerCtr != null && glossaryMarkerCtr.isTextMarkingEnabled()) { glossaryMarkerCtr.getInitialComponent().setDirty(true); } updateNextPrevious(); updateCourseDataAttributes(nclr.getCalledCourseNode()); } /** * implementation of listener which listens to publish events * * @see org.olat.core.util.event.GenericEventListener#event(org.olat.core.gui.control.Event) */ @Override public void event(Event event) { if (event instanceof PublishEvent) { PublishEvent pe = (PublishEvent)event; if(course.getResourceableId().equals(pe.getPublishedCourseResId())) { processPublishEvent(pe); } } else if (event instanceof OLATResourceableJustBeforeDeletedEvent) { OLATResourceableJustBeforeDeletedEvent ojde = (OLATResourceableJustBeforeDeletedEvent) event; // make sure it is our course (actually not needed till now, since we // registered only to one event, but good style. if (ojde.targetEquals(course, true)) { doDisposeAfterEvent(); } } else if (event instanceof AssessmentChangedEvent) { AssessmentChangedEvent ace = (AssessmentChangedEvent) event; Identity identity = uce.getIdentityEnvironment().getIdentity(); // reevaluate the changed node if the event changed the current user if (ace.getIdentityKey().equals(identity.getKey())) { String assessmentChangeType = ace.getCommand(); // do not re-evaluate things if only comment has been changed if (assessmentChangeType.equals(AssessmentChangedEvent.TYPE_SCORE_EVAL_CHANGED) || assessmentChangeType.equals(AssessmentChangedEvent.TYPE_ATTEMPTS_CHANGED)) { //LD: do not recalculate the score now, but at the next click, since the event comes before DB commit //uce.getScoreAccounting().evaluateAll(); assessmentChangedEventReceived = true; } // raise a flag to indicate refresh needsRebuildAfterRunDone = true; } } } private void processPublishEvent(PublishEvent pe) { if (pe.getState() == PublishEvent.PRE_PUBLISH) { // so far not interested in PRE PUBLISH event, but one could add user // and the currently active BB information. This in turn could be used // by the publish event issuer to decide whether or not to publish... } else if (pe.getState() == PublishEvent.PUBLISH) { // disable this controller and issue a information if (isInEditor) { needsRebuildAfterPublish = true; // author went in editor and published the course -> raise a flag to // later prepare the new // course to present him/her a nice view when // he/she closes the editor to return to the run main (this controller) } else if(getIdentity().getKey().equals(pe.getAuthorKey())) { //do nothing } else { if(currentCourseNode == null) { needsRebuildAfter = true; } else { try { String currentNodeIdent = currentCourseNode.getIdent(); Set<String> deletedNodeIds = pe.getDeletedCourseNodeIds(); Set<String> modifiedNodeIds = pe.getModifiedCourseNodeIds(); if(deletedNodeIds != null && deletedNodeIds.contains(currentNodeIdent)) { doDisposeAfterEvent(); } else if(modifiedNodeIds != null && modifiedNodeIds.contains(currentNodeIdent)) { doDisposeAfterEvent(); //needsRebuildAfter = true; } else { needsRebuildAfter = true; } } catch (Exception e) { logError("", e); //beyond update, be paranoid doDisposeAfterEvent(); } } } } } protected void doDisposeAfterEvent() { if(currentNodeController instanceof ConfigurationChangedListener) { //give to opportunity to close popups ... ((ConfigurationChangedListener)currentNodeController).configurationChanged(); } dispose(); } UserCourseEnvironmentImpl getUce() { return uce; } CourseNode getCurrentCourseNode() { return currentCourseNode; } /** * @see org.olat.core.gui.control.DefaultController#doDispose(boolean) */ protected void doDispose() { // remove as listener from this course events: // - group modification events CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(this, courseRepositoryEntry); // - publish changes // - assessment events CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(this, course); //remove as listener from course run eventAgency CoordinatorManager.getInstance().getCoordinator().getEventBus().deregisterFor(this, courseRunOres); // currentNodeController must be disposed manually does not work with // general BasicController methods if (currentNodeController != null && !currentNodeController.isDisposed()) { currentNodeController.dispose(); } currentNodeController = null; navHandler.dispose(); // log to Statistical and User log ThreadLocalUserActivityLogger.log(CourseLoggingAction.COURSE_LEAVING, getClass()); // log the fact that the user is leaving the course in the log file logAudit("Leaving course: [[["+courseTitle+"]]]", course.getResourceableId().toString()); } @Override public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { if(entries == null || entries.isEmpty()) { if(currentNodeController != null) { addToHistory(ureq, currentNodeController); } else { addToHistory(ureq, this); } return; } ContextEntry firstEntry = entries.get(0); String type = firstEntry.getOLATResourceable().getResourceableTypeName(); if("CourseNode".equalsIgnoreCase(type) || "Part".equalsIgnoreCase(type)) { CourseNode cn = course.getRunStructure().getNode(firstEntry.getOLATResourceable().getResourceableId().toString()); if(currentCourseNode == null || !currentCourseNode.equals(cn)) { getWindowControl().makeFlat(); // add logging information for case course gets started via jump-in // link/search addLoggingResourceable(LoggingResourceable.wrap(course)); if (cn != null) { addLoggingResourceable(LoggingResourceable.wrap(cn)); } // consume our entry if(entries.size() > 1) { entries = entries.subList(1, entries.size()); } updateTreeAndContent(ureq, cn, null, entries, firstEntry.getTransientState()); } else if (currentCourseNode.equals(cn)) { // consume our entry if(entries.size() > 1) { entries = entries.subList(1, entries.size()); } // the node to be activated is the one that is already on the screen if (currentNodeController instanceof Activateable2) { Activateable2 activateable = (Activateable2) currentNodeController; activateable.activate(ureq, entries, state); } } } } public void disableToolController(boolean disable) { toolbarPanel.setToolbarEnabled(!disable); } public void disableCourseClose(boolean disable) { toolbarPanel.setShowCloseLink(true, !disable); } @Override public void setDisposedMsgController(Controller disposeMsgController) { super.setDisposedMsgController(disposeMsgController); } }