/** * <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.statistics; import static org.olat.ims.qti.statistics.ui.StatisticFormatter.duration; import static org.olat.ims.qti.statistics.ui.StatisticFormatter.format; import static org.olat.ims.qti.statistics.ui.StatisticFormatter.getModeString; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import org.olat.core.commons.fullWebApp.popup.BaseFullWebappPopupLayoutFactory; 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.BarSeries.Stringuified; import org.olat.core.gui.components.chart.StatisticsComponent; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.link.LinkFactory; import org.olat.core.gui.components.link.LinkPopupSettings; import org.olat.core.gui.components.stack.TooledController; import org.olat.core.gui.components.stack.TooledStackedPanel; import org.olat.core.gui.components.stack.TooledStackedPanel.Align; 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.creator.ControllerCreator; import org.olat.core.gui.control.generic.dtabs.Activateable2; import org.olat.core.gui.media.MediaResource; import org.olat.core.id.context.ContextEntry; import org.olat.core.id.context.StateEntry; import org.olat.core.util.CodeHelper; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.course.nodes.QTICourseNode; import org.olat.course.nodes.iq.IQEditController; import org.olat.ims.qti.statistics.QTIType; import org.olat.ims.qti.statistics.model.StatisticAssessment; import org.olat.ims.qti.statistics.ui.QTI12AssessmentStatisticsController.ItemInfos; import org.olat.ims.qti21.QTI21StatisticsManager; import org.olat.ims.qti21.model.statistics.AssessmentItemStatistic; import org.olat.modules.assessment.ui.UserFilterController; import org.olat.modules.assessment.ui.event.UserFilterEvent; import org.springframework.beans.factory.annotation.Autowired; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; /** * * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class QTI21AssessmentTestStatisticsController extends BasicController implements Activateable2, TooledController { private final VelocityContainer mainVC; private final TooledStackedPanel stackPanel; private final Link printLink, downloadRawLink; private UserFilterController filterCtrl; private QTIType type; private QTICourseNode courseNode; private final QTI21StatisticResourceResult resourceResult; @Autowired private QTI21StatisticsManager qtiStatisticsManager; public QTI21AssessmentTestStatisticsController(UserRequest ureq, WindowControl wControl, TooledStackedPanel stackPanel, QTI21StatisticResourceResult resourceResult, boolean withFilter, boolean printMode) { super(ureq, wControl); this.stackPanel = stackPanel; this.resourceResult = resourceResult; courseNode = resourceResult.getTestCourseNode(); type = resourceResult.getType(); mainVC = createVelocityContainer("statistics_assessment_test"); mainVC.put("loadd3js", new StatisticsComponent("d3loader")); mainVC.contextPut("printMode", new Boolean(printMode)); if(resourceResult.getCourseEntry() != null) { mainVC.contextPut("courseId", resourceResult.getCourseEntry().getKey()); } mainVC.contextPut("testId", resourceResult.getTestEntry().getKey()); if(stackPanel != null) { printLink = LinkFactory.createToolLink("print" + CodeHelper.getRAMUniqueID(), translate("print"), this); printLink.setIconLeftCSS("o_icon o_icon_print o_icon-lg"); printLink.setPopup(new LinkPopupSettings(680, 500, "qti-stats")); stackPanel.addTool(printLink, Align.right); downloadRawLink = LinkFactory.createToolLink("download" + CodeHelper.getRAMUniqueID(), translate("download.raw.data"), this); stackPanel.addTool(downloadRawLink, Align.right); } else { printLink = null; downloadRawLink = LinkFactory.createLink("download.raw.data", mainVC, this); downloadRawLink.setCustomEnabledLinkCSS("o_content_download"); mainVC.put("download", downloadRawLink); } downloadRawLink.setIconLeftCSS("o_icon o_icon_download o_icon-lg"); if(withFilter && (resourceResult.canViewAnonymousUsers() || resourceResult.canViewNonParticipantUsers())) { filterCtrl = new UserFilterController(ureq, getWindowControl(), resourceResult.canViewNonParticipantUsers(), resourceResult.canViewAnonymousUsers(), resourceResult.isViewNonParticipantUsers(), resourceResult.isViewAnonymousUsers()); listenTo(filterCtrl); mainVC.put("filter", filterCtrl.getInitialComponent()); } putInitialPanel(mainVC); updateData(); } @Override protected void doDispose() { if(stackPanel != null) { stackPanel.removeTool(downloadRawLink); stackPanel.removeTool(printLink); } } @Override public void initTools() { if(stackPanel != null) { stackPanel.addTool(printLink, Align.right); stackPanel.addTool(downloadRawLink, Align.right); } } private void updateData() { StatisticAssessment stats = resourceResult.getQTIStatisticAssessment(); initScoreHistogram(stats); initScoreStatisticPerItem(stats.getNumOfParticipants()); initDurationHistogram(stats); initCourseNodeInformation(stats); } private Float getMaxScoreSetting(QTICourseNode testNode) { Float maxScoreSetting = null; if(QTIType.qtiworks.equals(type)) { Object maxScoreObj = testNode == null ? null : testNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_MAXSCORE); if (maxScoreObj instanceof Float) { maxScoreSetting = (Float)maxScoreObj; } else { // try to calculate max float max = 0; /*for (Item item: items) { if(item.getQuestion() != null) { max += item.getQuestion().getMaxValue(); } }*/ maxScoreSetting = max > 0 ? max : null; } } return maxScoreSetting; } private Float getCutValueSetting(QTICourseNode testNode) { Float cutValueSetting = null; if(QTIType.qtiworks.equals(type)) { Object cutScoreObj = testNode == null ? null : testNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_CUTVALUE); if (cutScoreObj instanceof Float) { cutValueSetting = (Float)cutScoreObj; } } return cutValueSetting; } private void initCourseNodeInformation(StatisticAssessment stats) { mainVC.contextPut("numOfParticipants", stats.getNumOfParticipants()); mainVC.contextPut("type", QTIType.qtiworks); mainVC.contextPut("numOfPassed", stats.getNumOfPassed()); mainVC.contextPut("numOfFailed", stats.getNumOfFailed()); mainVC.contextPut("average", format(stats.getAverage())); mainVC.contextPut("range", format(stats.getRange())); mainVC.contextPut("standardDeviation", format(stats.getStandardDeviation())); mainVC.contextPut("mode", getModeString(stats.getMode())); mainVC.contextPut("median", format(stats.getMedian())); String duration = duration(stats.getAverageDuration()); mainVC.contextPut("averageDuration", duration); Float maxScore = getMaxScoreSetting(courseNode); mainVC.contextPut("maxScore", maxScore == null ? "-" : format(maxScore)); Float cutValue = getCutValueSetting(courseNode); mainVC.contextPut("cutScore", cutValue == null ? "-" : format(cutValue)); } private void initScoreHistogram(StatisticAssessment stats) { int numOfParticipants = stats.getNumOfParticipants(); VelocityContainer scoreHistogramVC = createVelocityContainer("histogram_score"); scoreHistogramVC.setVisible(numOfParticipants > 0); scoreHistogramVC.contextPut("datas", BarSeries.datasToString(stats.getScores())); mainVC.put("scoreHistogram", scoreHistogramVC); } private void initDurationHistogram(StatisticAssessment stats) { boolean visible = BarSeries.hasNotNullDatas(stats.getDurations()) && stats.getNumOfParticipants() > 0; VelocityContainer durationHistogramVC = createVelocityContainer("histogram_duration"); durationHistogramVC.setVisible(visible); if(visible) { durationHistogramVC.contextPut("datas", BarSeries.datasToString(stats.getDurations())); } mainVC.put("durationHistogram", durationHistogramVC); } private void initScoreStatisticPerItem(double numOfParticipants) { BarSeries d1 = new BarSeries(); BarSeries d2 = new BarSeries(); List<AssessmentItemStatistic> statisticItems = qtiStatisticsManager .getStatisticPerItem(resourceResult.getResolvedAssessmentTest(), resourceResult.getSearchParams(), numOfParticipants); int i = 0; List<ItemInfos> itemInfos = new ArrayList<>(statisticItems.size()); for (AssessmentItemStatistic statisticItem: statisticItems) { AssessmentItem item = statisticItem.getAssessmentItem(); String label = Integer.toString(++i); String text = item.getTitle(); d1.add(statisticItem.getAverageScore(), label); d2.add(statisticItem.getNumOfCorrectAnswers(), label); itemInfos.add(new ItemInfos(label, text)); } mainVC.contextPut("itemInfoList", itemInfos); VelocityContainer averageScorePeritemVC = createVelocityContainer("hbar_average_score_per_item"); Stringuified data1 = BarSeries.getDatasAndColors(Collections.singletonList(d1), "bar_default"); averageScorePeritemVC.contextPut("datas", data1); mainVC.put("averageScorePerItemChart", averageScorePeritemVC); VelocityContainer percentRightAnswersPerItemVC = createVelocityContainer("hbar_right_answer_per_item"); Stringuified data2 = BarSeries.getDatasAndColors(Collections.singletonList(d2), "bar_green"); percentRightAnswersPerItemVC.contextPut("datas", data2); percentRightAnswersPerItemVC.contextPut("numOfParticipants", Long.toString(Math.round(numOfParticipants))); mainVC.put("percentRightAnswersPerItemChart", percentRightAnswersPerItemVC); } @Override public void activate(UserRequest ureq, List<ContextEntry> entries, StateEntry state) { // } @Override protected void event(UserRequest ureq, Controller source, Event event) { if(filterCtrl == source) { if(event instanceof UserFilterEvent) { UserFilterEvent ufe = (UserFilterEvent)event; resourceResult.setViewAnonymousUsers(ufe.isWithAnonymousUser()); resourceResult.setViewNonPaticipantUsers(ufe.isWithNonParticipantUsers()); updateData(); } } super.event(ureq, source, event); } @Override protected void event(UserRequest ureq, Component source, Event event) { if(printLink == source) { printPages(ureq); } else if(downloadRawLink == source) { doDownloadRawData(ureq); } } private void printPages(UserRequest ureq) { ControllerCreator printControllerCreator = new ControllerCreator() { @Override public Controller createController(UserRequest lureq, WindowControl lwControl) { return new QTI21PrintController(lureq, lwControl, resourceResult); } }; ControllerCreator layoutCtrlr = BaseFullWebappPopupLayoutFactory.createPrintPopupLayout(printControllerCreator); openInNewBrowserWindow(ureq, layoutCtrlr); } private void doDownloadRawData(UserRequest ureq) { String label; if(courseNode == null) { label = StringHelper.transformDisplayNameToFileSystemName(resourceResult.getTestEntry().getDisplayname()); } else { label = courseNode.getType() + "_" + StringHelper.transformDisplayNameToFileSystemName(courseNode.getShortName()); } label += "_" + Formatter.formatDatetimeFilesystemSave(new Date()) + ".xlsx"; MediaResource resource = new QTI21StatisticsResource(resourceResult, label, getLocale()); ureq.getDispatchResult().setResultingMediaResource(resource); } }