/** * <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.course.highscore.ui; /** * Initial Date: 10.08.2016 <br> * @author fkiefer */ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.chart.BarSeries; import org.olat.core.gui.components.chart.StatisticsComponent; import org.olat.core.gui.components.form.flexible.FormItemContainer; import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement; import org.olat.core.gui.components.form.flexible.impl.Form; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableCssDelegate; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelImpl; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableRendererType; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.link.LinkFactory; 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.generic.closablewrapper.CloseableCalloutWindowController; import org.olat.core.id.Identity; import org.olat.core.util.prefs.Preferences; import org.olat.course.assessment.AssessmentManager; import org.olat.course.highscore.manager.HighScoreManager; import org.olat.course.highscore.model.HighScoreRankingResults; import org.olat.course.nodes.CourseNode; import org.olat.course.nodes.MSCourseNode; import org.olat.course.nodes.STCourseNode; import org.olat.course.run.userview.UserCourseEnvironment; import org.olat.modules.ModuleConfiguration; import org.olat.modules.assessment.AssessmentEntry; import org.olat.user.DisplayPortraitController; import org.olat.user.UserAvatarMapper; import org.olat.user.UserManager; import org.springframework.beans.factory.annotation.Autowired; public class HighScoreRunController extends FormBasicController{ private static final String GUIPREF_KEY_HIGHSCORE = "highscore"; private FlexiTableDataModel<HighScoreTableEntry> tableDataModel, tableDataModel2; private List<HighScoreTableEntry> allMembers, ownIdMembers; private List<List<HighScoreTableEntry>> allPodium; private List<Integer> ownIdIndices; private int tableSize; private Identity ownIdentity; private boolean viewTable, viewPosition, viewHistogram, viewPodium, viewHighscore, anonymous, isSTCourseNode; private double[] allScores; private Link[] links = new Link[3]; private CloseableCalloutWindowController calloutCtr; private Float lowerBorder, upperBorder; private HighScoreRankingResults highscoreDataModel; private String nodeID; @Autowired private HighScoreManager highScoreManager; @Autowired private UserManager userManager; /** * Instantiates a new high score run controller. * Use this controller in combination with FormBasicController */ public HighScoreRunController(UserRequest ureq, WindowControl wControl, UserCourseEnvironment userCourseEnv, CourseNode courseNode, Form mainForm) { super(ureq, wControl, LAYOUT_CUSTOM, "highscore", mainForm); this.nodeID = courseNode.getIdent(); setupContent(ureq, userCourseEnv, courseNode); } /** * Instantiates a new high score run controller. * Use this controller in combination with BasicController and DefaultController */ public HighScoreRunController(UserRequest ureq, WindowControl wControl, UserCourseEnvironment userCourseEnv, CourseNode courseNode) { super(ureq, wControl, "highscore"); this.nodeID = courseNode.getIdent(); this.isSTCourseNode = courseNode instanceof STCourseNode; setupContent(ureq, userCourseEnv, courseNode); } private void setupContent(UserRequest ureq, UserCourseEnvironment userCourseEnv, CourseNode courseNode) { //initialize ModuleConfiguration ModuleConfiguration config = courseNode.getModuleConfiguration(); viewHighscore = config.getBooleanSafe(HighScoreEditController.CONFIG_KEY_HIGHSCORE); // do not build form if high-score is not set if (!viewHighscore){ return; } Date start = config.getBooleanEntry(HighScoreEditController.CONFIG_KEY_DATESTART) != null ? (Date) config.get(HighScoreEditController.CONFIG_KEY_DATESTART) : null; // display only if start time has been met if (start != null && start.after(new Date())){ viewHighscore = false; return; } // guests will never see the highscore if (ureq != null && ureq.getUserSession().getRoles().isGuestOnly()){ viewHighscore = false; return; } ownIdentity = userCourseEnv.getIdentityEnvironment().getIdentity(); AssessmentManager assessmentManager = userCourseEnv.getCourseEnvironment().getAssessmentManager(); AssessmentEntry ownEntry = assessmentManager.getAssessmentEntry(courseNode, ownIdentity); // check user visibility if (ownEntry != null && ownEntry.getUserVisibility() != null && !ownEntry.getUserVisibility().booleanValue()) { viewHighscore = false; return; } boolean adminORcoach = userCourseEnv.isAdmin() || userCourseEnv.isCoach(); // ban zero scorer from viewing the highscore on STCourseNode if(!adminORcoach && isSTCourseNode && ownEntry != null && ownEntry.getScore().equals(new BigDecimal(0))) { viewHighscore = false; return; } // coaches or admin may see highscore, user only if already scored if (!adminORcoach && (ownEntry == null || (ownEntry != null && ownEntry.getScore() == null))) { viewHighscore = false; return; } List<AssessmentEntry> assessEntries = assessmentManager.getAssessmentEntriesWithStatus(courseNode, null, isSTCourseNode); // display only if has content if (assessEntries == null || assessEntries.isEmpty()) { viewHighscore = false; return; } viewTable = config.getBooleanSafe(HighScoreEditController.CONFIG_KEY_LISTING); viewHistogram = config.getBooleanSafe(HighScoreEditController.CONFIG_KEY_HISTOGRAM); viewPosition = config.getBooleanSafe(HighScoreEditController.CONFIG_KEY_POSITION); viewPodium = config.getBooleanSafe(HighScoreEditController.CONFIG_KEY_PODIUM); anonymous = config.getBooleanSafe(HighScoreEditController.CONFIG_KEY_ANONYMIZE); int bestOnly = config.getBooleanEntry(HighScoreEditController.CONFIG_KEY_BESTONLY) != null ? (int) config.get(HighScoreEditController.CONFIG_KEY_BESTONLY) : 0; tableSize = bestOnly != 0 ? (int) config.get(HighScoreEditController.CONFIG_KEY_NUMUSER) : assessEntries.size(); initLists(); // get borders lowerBorder = (Float)config.get(MSCourseNode.CONFIG_KEY_SCORE_MIN); upperBorder = (Float)config.get(MSCourseNode.CONFIG_KEY_SCORE_MAX); // compute ranking and order highscoreDataModel = highScoreManager.sortRankByScore(assessEntries, allMembers, ownIdMembers, allPodium, ownIdIndices, tableSize, ownIdentity, userManager); allScores = highscoreDataModel.getScores(); // init showConfig from user Prefs doLoadShowConfig(ureq); initForm(ureq); } /** * loads GUI preferences */ private void doLoadShowConfig(UserRequest ureq) { // add as listener to form layout for later dispatchinf of gui prefs changes this.flc.getFormItemComponent().addListener(this); // init showConfig from user prefs Boolean showConfig = Boolean.TRUE; if (ureq != null) { Preferences guiPrefs = ureq.getUserSession().getGuiPreferences(); showConfig = (Boolean) guiPrefs.get(HighScoreRunController.class, GUIPREF_KEY_HIGHSCORE + nodeID); if (showConfig == null) { showConfig = Boolean.TRUE; } } // expose initial value to velocity this.flc.contextPut("showConfig", Boolean.valueOf(showConfig)); } private void doUpdateShowConfig(UserRequest ureq, boolean newValue) { // save new config in GUI prefs Preferences guiPrefs = ureq.getUserSession().getGuiPreferences(); if (guiPrefs != null) { guiPrefs.putAndSave(HighScoreRunController.class, GUIPREF_KEY_HIGHSCORE + nodeID, Boolean.valueOf(newValue)); } } @Override protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { VelocityContainer mainVC = this.flc.getFormItemComponent(); mainVC.put("loadd3js", new StatisticsComponent("d3loader")); if (viewHistogram) { VelocityContainer scoreHistogramVC = createVelocityContainer("histogram_score"); //transfer all scores to velocity container as base data for histogram HighScoreRankingResults modifiedData = highScoreManager.processHistogramData(allScores, lowerBorder, upperBorder); allScores = modifiedData.getModifiedScores(); scoreHistogramVC.contextPut("datas", BarSeries.datasToString(allScores)); //histogram marker for own position scoreHistogramVC.contextPut("cutValue", ownIdIndices.size() > 0 ? highScoreManager.calculateHistogramCutvalue( allMembers.get(ownIdIndices.get(0)).getScore(), modifiedData.getClasswidth(), modifiedData.getMin()) : -1000); //classwidth to correctly display the histogram long classwidth = modifiedData.getClasswidth(); scoreHistogramVC.contextPut("step", classwidth); //find path for ownID image to display in histogram UserAvatarMapper mapper = new UserAvatarMapper(false); String mapperPath = registerMapper(ureq, mapper); String identityMapperPath = mapper.createPathFor(mapperPath, ownIdentity); scoreHistogramVC.contextPut("mapperUrl", identityMapperPath); mainVC.put("scoreHistogram", scoreHistogramVC); } if (viewPodium) { String[] localizer = { "first", "second", "third" }; // for clarity and space reasons do not show any portraits if one position has more than 6 persons int maxPerson = 6; boolean showPortraits = !anonymous && allPodium.get(0).size() <= maxPerson && allPodium.get(1).size() <= maxPerson && allPodium.get(2).size() <= maxPerson; for (int i = 0; i < localizer.length; i++) { int sizePerPos = allPodium.get(i).size(); StringBuilder sb = new StringBuilder(); if (sizePerPos > 2){ int reduce = 0; //create link if podium has more than 2 entries per rank, entries can be displayed as anonymous if (allPodium.get(i).get(0).getIdentity().equals(ownIdentity)) { sb.append(userManager.getUserDisplayName(ownIdentity)); ++reduce; } if (sizePerPos > 6 || anonymous) { mainVC.contextPut("further" + (i + 1), (sizePerPos - reduce) + " " + (reduce == 1 ? translate("highscore.further") : translate("highscore.total"))); } else { links[i] = LinkFactory.createLink(null, "link" + (i + 1), "cmd", (sizePerPos - reduce) + " " + (reduce == 1 ? translate("highscore.further") : translate("highscore.total")), getTranslator(), mainVC, this, 16); } } else { for (HighScoreTableEntry te : allPodium.get(i)) { if (!anonymous || te.getIdentity().equals(ownIdentity)) { sb.append(userManager.getUserDisplayName(te.getIdentity())); sb.append("<br/>"); } } } mainVC.contextPut(localizer[i], sizePerPos > 0 ? sb.toString() : translate("highscore.unavail")); mainVC.contextPut("score" + (i + 1), sizePerPos > 0 ? allPodium.get(i).get(0).getScore() : null); if (sizePerPos > 0 && showPortraits) { //decide whether or not to display id portrait mainVC.contextPut("number"+ (i + 1), sizePerPos); for (int j = 0; j < sizePerPos; j++) { Identity currentID = allPodium.get(i).get(j).getIdentity(); boolean choosePortrait = !anonymous || ownIdentity.equals(currentID); DisplayPortraitController portrait = new DisplayPortraitController(ureq, getWindowControl(), currentID, false, choosePortrait, !choosePortrait); Component portraitComponent = portrait.getInitialComponent(); mainVC.put("portrait" + (i + 1) + "-" + (j + 1), portraitComponent); } } else { // if amount of people per rank is too large, own id portrait can still be displayed for (int j = 0; j < sizePerPos; j++) { Identity currentID = allPodium.get(i).get(j).getIdentity(); if (ownIdentity.equals(currentID)) { DisplayPortraitController portrait = new DisplayPortraitController(ureq, getWindowControl(), currentID, true, true, false); mainVC.put("portrait" + (i + 1), portrait.getInitialComponent()); } } } } } if (viewTable) { FlexiTableColumnModel tableColumnModel = FlexiTableDataModelFactory.createFlexiTableColumnModel(); tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel("highscore.table.header1", HighScoreTableEntry.RANK)); tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel("highscore.table.header2", HighScoreTableEntry.SCORE)); tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel("highscore.table.header3", HighScoreTableEntry.NAME)); //trim to tableSize if (tableSize < allMembers.size()) { allMembers.subList(tableSize, allMembers.size()).clear(); } tableDataModel = new FlexiTableDataModelImpl<HighScoreTableEntry>(new HighScoreFlexiTableModel(allMembers, anonymous, translate("highscore.anonymous"),ownIdentity), tableColumnModel); FlexiTableElement topTenTable = uifactory.addTableElement(getWindowControl(), "table", tableDataModel, getTranslator(), formLayout); topTenTable.setNumOfRowsEnabled(false); topTenTable.setCustomizeColumns(false); topTenTable.setCssDelegate(new MarkedMemberCssDelegate(false)); //establish a 2nd table if ownID position is greater than first table's size setting if (!ownIdMembers.isEmpty()) { tableDataModel2 = new FlexiTableDataModelImpl<HighScoreTableEntry>( new HighScoreFlexiTableModel(ownIdMembers, anonymous, translate("highscore.anonymous"), ownIdentity), tableColumnModel); FlexiTableElement tableElement = uifactory.addTableElement( getWindowControl(), "table2", tableDataModel2, getTranslator(), formLayout); tableElement.setNumOfRowsEnabled(false); tableElement.setCustomizeColumns(false); tableElement.setCssDelegate(new MarkedMemberCssDelegate(true)); } } if (viewPosition && ownIdIndices.size() > 0) { int amountWorse = allScores.length - ownIdIndices.get(0) - 1; if (amountWorse > 0) { mainVC.contextPut("relposition", translate("highscore.position.inrelation", new String[] { String.valueOf(amountWorse)})); } int ownRank = highscoreDataModel.getOwnTableEntry().getRank(); mainVC.contextPut("position", translate("highscore.position.yourposition", new String[] { String.valueOf(ownRank) })); } } private void initLists(){ ownIdIndices = new ArrayList<>(); allMembers = new ArrayList<>(); ownIdMembers = new ArrayList<>(); allPodium = new ArrayList<>(); allPodium.add(new ArrayList<>()); allPodium.add(new ArrayList<>()); allPodium.add(new ArrayList<>()); } /** * Builds the member list, which is displayed for each rank's link * if the chosen rank has more than 2 entries * * @param persons the persons in i-th rank * @param i the rank of the podium */ private void buildMemberList(List<String> persons, int i){ int counter = 0; for (HighScoreTableEntry te : allPodium.get(i)) { if (!te.getIdentity().equals(ownIdentity)) { String person = te.getName(); persons.add(person); } if(counter++ == 6) { persons.add(translate("highscore.further")); break; } } } @Override public void event(UserRequest ureq, Component source, Event event) { if (source == links[0] || source == links[1] || source == links[2]) { List<String> persons = new ArrayList<>(); Link link; if (source == links[0]){ link = links[0]; buildMemberList(persons,0); } else if (source == links[1]) { link = links[1]; buildMemberList(persons,1); } else { link = links[2]; buildMemberList(persons,2); } if (calloutCtr == null) { VelocityContainer podiumcalloutVC = createVelocityContainer("podiumcallout"); podiumcalloutVC.contextPut("persons", persons); calloutCtr = new CloseableCalloutWindowController(ureq, getWindowControl(), podiumcalloutVC, link, "This is a title in a callout window", false, null); calloutCtr.activate(); listenTo(calloutCtr); } else { removeAsListenerAndDispose(calloutCtr); calloutCtr = null; } } else if (source == this.flc.getFormItemComponent()) { if("show".equals(event.getCommand())) { doUpdateShowConfig(ureq, true); } else if("hide".equals(event.getCommand())) { doUpdateShowConfig(ureq, false); } } } @Override protected void formOK(UserRequest ureq) { // only formInnerEvent() } @Override protected void doDispose() { // only formInnerEvent() } private class MarkedMemberCssDelegate extends DefaultFlexiTableCssDelegate { private boolean mark; public MarkedMemberCssDelegate(boolean mark) { this.mark = mark; } @Override public String getRowCssClass(FlexiTableRendererType type, int pos) { return ownIdIndices.size() > 0 && (ownIdIndices.get(0) < tableSize && pos == ownIdIndices.get(0)) || mark ? "o_row_selected" : null; } } public boolean isViewHighscore() { return viewHighscore; } }