diff --git a/src/main/java/org/olat/modules/quality/analysis/QualityAnalysisService.java b/src/main/java/org/olat/modules/quality/analysis/QualityAnalysisService.java index 47481ac66c0b50b640f97336e16ba02800d89684..8dc5a5f3087b0f8fd0267c2ff2f8f8f52889fae9 100644 --- a/src/main/java/org/olat/modules/quality/analysis/QualityAnalysisService.java +++ b/src/main/java/org/olat/modules/quality/analysis/QualityAnalysisService.java @@ -113,7 +113,19 @@ public interface QualityAnalysisService { public MultiTrendSeries<MultiKey> calculateTrends(AnalysisSearchParameter searchParams, Set<Rubric> rubrics, MultiGroupBy groupBy, TemporalGroupBy temporalGroupBy); - public HeatMapStatistic calculateTotal(List<HeatMapStatistic> statistics, Rubric rubric); + /** + * Calculate the statistic for all sliders of all rubrics. Before using this method, make sure, + * that all rubrics are identically configured to get accurate results (same + * scale, number of steps, good end, ...)! + * + * @param statistics + * @param rubrics + * @return + */ + public HeatMapStatistic calculateRubricsTotal(List<? extends GroupedStatistic> statistics, Collection<Rubric> rubrics); + + public HeatMapStatistic calculateSliderTotal(List<? extends HeatMapStatistic> statistics, Rubric rubric); public boolean isInsufficient(Rubric rubric, Double avg); + } diff --git a/src/main/java/org/olat/modules/quality/analysis/manager/AnalysisFilterDAO.java b/src/main/java/org/olat/modules/quality/analysis/manager/AnalysisFilterDAO.java index c393944bbf28b1ef94b4cdc97d4e5e798ec1a4b5..3e87dbaceff2a5b0755c125fc6aed8622451743f 100644 --- a/src/main/java/org/olat/modules/quality/analysis/manager/AnalysisFilterDAO.java +++ b/src/main/java/org/olat/modules/quality/analysis/manager/AnalysisFilterDAO.java @@ -393,7 +393,7 @@ public class AnalysisFilterDAO { sb.append(groupByIdentifier? " response.responseIdentifier": " cast(null as string)"); appendGroupBys(sb, multiGroupBy, true); appendTemporalGroupBy(sb, temporalGroupBy, true); - sb.append(" , count(response)"); + sb.append(" , count(distinct response.key)"); sb.append(" , avg(response.numericalResponse)"); sb.append(" )"); appendFrom(sb, searchParams); diff --git a/src/main/java/org/olat/modules/quality/analysis/manager/QualityAnalysisServiceImpl.java b/src/main/java/org/olat/modules/quality/analysis/manager/QualityAnalysisServiceImpl.java index 06540c9222d0c8f7f9a125c8008fba239a5f7aac..6ebb07fa0f1d0a99fa004e232af3e9a001e51304 100644 --- a/src/main/java/org/olat/modules/quality/analysis/manager/QualityAnalysisServiceImpl.java +++ b/src/main/java/org/olat/modules/quality/analysis/manager/QualityAnalysisServiceImpl.java @@ -344,8 +344,14 @@ public class QualityAnalysisServiceImpl implements QualityAnalysisService { } @Override - public HeatMapStatistic calculateTotal(List<HeatMapStatistic> statistics, Rubric rubric) { - return statisticsCalculator.calculateTotal(statistics, rubric); + public HeatMapStatistic calculateRubricsTotal(List<? extends GroupedStatistic> statistics, + Collection<Rubric> rubric) { + return statisticsCalculator.calculateRubricsTotal(statistics, rubric); + } + + @Override + public HeatMapStatistic calculateSliderTotal(List<? extends HeatMapStatistic> statistics, Rubric rubrics) { + return statisticsCalculator.calculateSliderTotal(statistics, rubrics); } @Override diff --git a/src/main/java/org/olat/modules/quality/analysis/manager/StatisticsCalculator.java b/src/main/java/org/olat/modules/quality/analysis/manager/StatisticsCalculator.java index e748e277d47ed251243bb0c5f554ad99d8c1b899..0477c37ae5547b909d208c360121d0a503b320fc 100644 --- a/src/main/java/org/olat/modules/quality/analysis/manager/StatisticsCalculator.java +++ b/src/main/java/org/olat/modules/quality/analysis/manager/StatisticsCalculator.java @@ -115,8 +115,50 @@ public class StatisticsCalculator { log.debug("Grouped statistic: " + statistic.toString()); return statistic; } + + public HeatMapStatistic calculateRubricsTotal(List<? extends GroupedStatistic> statistics, + Collection<Rubric> rubrics) { + Rubric firstRubric = null; + HeatMapStatistic total; + long count = 0; + long sumCount = 0; + double sumValues = 0; + for (Rubric rubric: rubrics) { + if (firstRubric == null) { + firstRubric = rubric; + } + for (Slider slider: rubric.getSliders()) { + GroupedStatistic statistic = getStatistic(statistics, slider); + if (statistic != null) { + Long statisticCount = statistic.getCount(); + if (statisticCount != null) { + count += statisticCount.longValue(); + sumCount += statisticCount.longValue() * slider.getWeight().intValue(); + sumValues += statisticCount.longValue() * statistic.getAvg().doubleValue() * slider.getWeight().intValue(); + } + } + } + } + if (count == 0) { + total = new HeatMapStatisticImpl(null, null, null); + } else { + double avg = sumValues / sumCount; + RubricRating rating = evaluationFormManager.getRubricRating(firstRubric, avg); + total = new HeatMapStatisticImpl(count, avg, rating); + } + return total; + } - HeatMapStatistic calculateTotal(List<HeatMapStatistic> statistics, Rubric rubric) { + private GroupedStatistic getStatistic(List<? extends GroupedStatistic> statistics, Slider slider) { + for (GroupedStatistic statistic : statistics) { + if (statistic != null && slider.getId().equals(statistic.getIdentifier())) { + return statistic; + } + } + return null; + } + + HeatMapStatistic calculateSliderTotal(List<? extends HeatMapStatistic> statistics, Rubric rubric) { HeatMapStatistic total; long count = 0; double sumValues = 0; diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/AnalysisUIFactory.java b/src/main/java/org/olat/modules/quality/analysis/ui/AnalysisUIFactory.java index 7cfcdd7274cb376fe8fc2a7f697d1d3d5f634ef6..a282164c8001b8e4fd47e109ab070a41d6faaddd 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/AnalysisUIFactory.java +++ b/src/main/java/org/olat/modules/quality/analysis/ui/AnalysisUIFactory.java @@ -210,7 +210,7 @@ class AnalysisUIFactory { return keyValues; } - private static boolean areIdenticalRubrics(List<Rubric> rubrics) { + static boolean areIdenticalRubrics(List<Rubric> rubrics) { Rubric master = rubrics.get(0); for (int i = 1; i < rubrics.size(); i++) { Rubric rubric = rubrics.get(i); diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/FooterGroupByDataModel.java b/src/main/java/org/olat/modules/quality/analysis/ui/FooterGroupByDataModel.java index e578b5c2464e3faf0f297f878ef32d38d72f45cc..c297faa48cea7d118ed158b2d285787f788dae15 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/FooterGroupByDataModel.java +++ b/src/main/java/org/olat/modules/quality/analysis/ui/FooterGroupByDataModel.java @@ -25,6 +25,7 @@ import java.util.Locale; import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiTableDataModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel; import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableFooterModel; +import org.olat.modules.quality.analysis.HeatMapStatistic; /** * @@ -36,15 +37,17 @@ class FooterGroupByDataModel extends GroupByDataModel implements FlexiTableFoote private final String footerHeader; private List<?> footerDataValues; + private Object footerTotal; FooterGroupByDataModel(FlexiTableColumnModel columnsModel, Locale locale, String footerHeader) { super(columnsModel, locale); this.footerHeader = footerHeader; } - public void setObjects(List<GroupByRow> objects, List<?> footerDataValues) { + public void setObjects(List<GroupByRow> objects, List<?> footerDataValues, HeatMapStatistic footerTotal) { super.setObjects(objects); this.footerDataValues = footerDataValues; + this.footerTotal = footerTotal; } @Override @@ -63,6 +66,9 @@ class FooterGroupByDataModel extends GroupByDataModel implements FlexiTableFoote int pos = col - GroupByController.DATA_OFFSET; return footerDataValues.get(pos); } + if (col == GroupByController.TOTAL_OFFSET) { + return footerTotal; + } return null; } diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/GroupByController.java b/src/main/java/org/olat/modules/quality/analysis/ui/GroupByController.java index 7d2538f032a18103eaf1cf4096157fe9e494d843..5054e6f17f059140aad07c7fe3f1792484bb5395 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/GroupByController.java +++ b/src/main/java/org/olat/modules/quality/analysis/ui/GroupByController.java @@ -98,7 +98,8 @@ import org.springframework.beans.factory.annotation.Autowired; */ public abstract class GroupByController extends FormBasicController implements FilterableController { - public static final int DATA_OFFSET = 10; + public static final int TOTAL_OFFSET = 99; + public static final int DATA_OFFSET = 100; private static final String CMD_GROUP_PREFIX = "CLICKED_"; private static final String CMD_TREND = "TREND"; @@ -106,7 +107,7 @@ public abstract class GroupByController extends FormBasicController implements F private static final Collection<GroupBy> GROUP_BY_TOPICS = Arrays.asList(GroupBy.TOPIC_IDENTITY, GroupBy.TOPIC_ORGANISATION, GroupBy.TOPIC_CURRICULUM, GroupBy.TOPIC_CURRICULUM_ELEMENT, GroupBy.TOPIC_REPOSITORY); - + private TooledStackedPanel stackPanel; private ToolComponents toolComponents; private FormLayoutContainer groupingCont; @@ -255,6 +256,8 @@ public abstract class GroupByController extends FormBasicController implements F protected abstract Set<MultiKey> getStatisticsMultiKeys(); + protected abstract void addTotalDataColumn(FlexiTableColumnModel columnsModel, int columnIndex); + protected abstract boolean hasFooter(); protected abstract void initModel(FlexiTableColumnModel columnsModel); @@ -453,6 +456,7 @@ public abstract class GroupByController extends FormBasicController implements F } addDataColumns(columnsModel, DATA_OFFSET); + addTotalDataColumn(columnsModel, TOTAL_OFFSET); DefaultFlexiColumnModel trendColumn = new DefaultFlexiColumnModel("heatmap.table.title.trend", columnIndex++, CMD_TREND, new StaticFlexiCellRenderer("", CMD_TREND, "o_icon o_icon-lg o_icon_qual_ana_trend", null)); @@ -906,6 +910,10 @@ public abstract class GroupByController extends FormBasicController implements F return rubric; } + public Slider getSlider() { + return slider; + } + public String getIdentifier() { return slider.getId(); } diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/GroupByDataModel.java b/src/main/java/org/olat/modules/quality/analysis/ui/GroupByDataModel.java index 5dd5c0271ee324180228d9587b5b7c054f2b52ba..6ecfaed5b983fda399c74cfdc098d0f1d9454d23 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/GroupByDataModel.java +++ b/src/main/java/org/olat/modules/quality/analysis/ui/GroupByDataModel.java @@ -64,6 +64,9 @@ class GroupByDataModel extends DefaultFlexiTableDataModel<GroupByRow> return row.getStatistic(pos); } } + if (col == GroupByController.TOTAL_OFFSET) { + return row.getTotal(); + } if (col < row.getGroupNamesSize()) { return row.getGroupName(col); } diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/GroupByRow.java b/src/main/java/org/olat/modules/quality/analysis/ui/GroupByRow.java index bac31b7df9edb7039105b8035ec628e6268f3429..7782a63547232aab53de68b2b6768260ad1d6490 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/GroupByRow.java +++ b/src/main/java/org/olat/modules/quality/analysis/ui/GroupByRow.java @@ -35,6 +35,7 @@ public class GroupByRow { private final MultiKey multiKey; private final List<String> groupNames; private final List<? extends GroupedStatistic> statistics; + private Object total; public GroupByRow(MultiKey multiKey, List<String> groupNames, List<? extends GroupedStatistic> statistics) { this.multiKey = multiKey; @@ -65,5 +66,17 @@ public class GroupByRow { public GroupedStatistic getStatistic(int index) { return statistics.get(index); } + + List<? extends GroupedStatistic> getStatistics() { + return statistics; + } + + public void setTotal(Object total) { + this.total = total; + } + + public Object getTotal() { + return total; + } } diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/HeatMapController.java b/src/main/java/org/olat/modules/quality/analysis/ui/HeatMapController.java index 08f1377318c2b722111799373ec2af861a7309ac..53e6247beec208dfc203a78ba014d73b42f5032e 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/HeatMapController.java +++ b/src/main/java/org/olat/modules/quality/analysis/ui/HeatMapController.java @@ -20,6 +20,7 @@ package org.olat.modules.quality.analysis.ui; import static java.util.stream.Collectors.toList; +import static org.olat.modules.quality.analysis.ui.AnalysisUIFactory.areIdenticalRubrics; import java.util.ArrayList; import java.util.List; @@ -51,6 +52,7 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class HeatMapController extends GroupByController { + private final boolean identicalRubrics; private FooterGroupByDataModel dataModel; private GroupedStatistics<GroupedStatistic> statistics; private int maxCount; @@ -64,7 +66,8 @@ public class HeatMapController extends GroupByController { TrendDifference trendDifference, String rubricId) { super(ureq, wControl, stackPanel, filterCtrl, evaluationForm, availableAttributes, multiGroupBy, insufficientOnly, temporalGroupBy, trendDifference, rubricId); - + List<Rubric> rubrics = getSliders().stream().map(SliderWrapper::getRubric).distinct().collect(toList()); + this.identicalRubrics = areIdenticalRubrics(rubrics); } @Override @@ -109,6 +112,18 @@ public class HeatMapController extends GroupByController { return columnIndex; } + + @Override + protected void addTotalDataColumn(FlexiTableColumnModel columnsModel, int columnIndex) { + if (identicalRubrics) { + DefaultFlexiColumnModel columnModel = new DefaultFlexiColumnModel("", columnIndex, + HeatMapRenderer.variableSize(maxCount)); + columnModel.setHeaderLabel(translate("heatmap.table.title.average")); + columnModel.setFooterCellRenderer(HeatMapRenderer.fixedSize()); + columnsModel.addFlexiColumnModel(columnModel); + } + } + @Override protected List<? extends GroupedStatistic> getGroupedStatistcList(MultiKey multiKey) { // Iterate over the identifiers to sort the statistics according to the headers. @@ -142,6 +157,24 @@ public class HeatMapController extends GroupByController { @Override protected void setModelOjects(List<GroupByRow> rows) { + appendTotalColumn(rows); + List<HeatMapStatistic> footerStatistics = getFooterStatistics(rows); + HeatMapStatistic footerTotal = getFooterTotal(rows); + dataModel.setObjects(rows, footerStatistics, footerTotal); + } + + private void appendTotalColumn(List<GroupByRow> rows) { + if (identicalRubrics) { + List<Rubric> rubrics = getSliders().stream().map(SliderWrapper::getRubric).distinct().collect(toList()); + for (GroupByRow row : rows) { + List<? extends GroupedStatistic> rowStatistics = row.getStatistics(); + HeatMapStatistic total = analysisService.calculateRubricsTotal(rowStatistics, rubrics); + row.setTotal(total); + } + } + } + + private List<HeatMapStatistic> getFooterStatistics(List<GroupByRow> rows) { List<HeatMapStatistic> footerStatistics = new ArrayList<>(); if (!rows.isEmpty()) { int statisticsSize = rows.get(0).getStatisticsSize(); @@ -155,11 +188,27 @@ public class HeatMapController extends GroupByController { rubric = getRubric(columnStatistic.getIdentifier()); } } - HeatMapStatistic total = analysisService.calculateTotal(columnStatistics, rubric); + HeatMapStatistic total = analysisService.calculateSliderTotal(columnStatistics, rubric); footerStatistics.add(total); } } - dataModel.setObjects(rows, footerStatistics); + return footerStatistics; + } + + private HeatMapStatistic getFooterTotal(List<GroupByRow> rows) { + HeatMapStatistic total = null; + if (identicalRubrics) { + ArrayList<HeatMapStatistic> columnStatistics = new ArrayList<>(rows.size()); + for (GroupByRow row : rows) { + Object rowTotal = row.getTotal(); + if (rowTotal instanceof HeatMapStatistic) { + columnStatistics.add((HeatMapStatistic)rowTotal); + } + } + Rubric rubric = getSliders().stream().map(SliderWrapper::getRubric).findFirst().get(); + total = analysisService.calculateSliderTotal(columnStatistics, rubric); + } + return total; } } diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/TrendController.java b/src/main/java/org/olat/modules/quality/analysis/ui/TrendController.java index 43060bb62505428f66a53096e40483dece7c1691..9d9dfeedf8ead0f00588d95f6fac80419bdd5bae 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/TrendController.java +++ b/src/main/java/org/olat/modules/quality/analysis/ui/TrendController.java @@ -87,6 +87,11 @@ public class TrendController extends GroupByController { } return 0; } + + @Override + protected void addTotalDataColumn(FlexiTableColumnModel columnsModel, int columnIndex) { + // + } private List<String> getTemporalHeaders() { List<TemporalKey> temporalKeys = multiTrendSeries.getTemporalKeys(); diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_de.properties index 20ca96d9a7980b06e6078f1dfa310ff07a5d1ea4..41dbd72f1de95dfb5999eed02d1546b552300b75 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_de.properties @@ -70,6 +70,7 @@ heatmap.legend.filters=Filter heatmap.legend.questions=Fragen heatmap.not.specified=n/a heatmap.table.slider.header=$org.olat.modules.forms.ui\:slider.label.code +heatmap.table.title.average=Durchschnitt heatmap.table.title.blank= heatmap.table.title.curriculum.element=Curriculumelement heatmap.table.title.curriculum.organisation=Organisation des Curriculum diff --git a/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_en.properties index a7d79d282c9b76fdd424f619abd175b4f9ad3fb4..6170846e9faf821003945ccce99ce48511ef68f5 100644 --- a/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/modules/quality/analysis/ui/_i18n/LocalStrings_en.properties @@ -70,6 +70,7 @@ heatmap.legend.filters=Filters heatmap.legend.questions=Questions heatmap.not.specified=n/a heatmap.table.slider.header=$org.olat.modules.forms.ui\:slider.label.code +heatmap.table.title.average=Average heatmap.table.title.blank= heatmap.table.title.curriculum.element=Curriculum element heatmap.table.title.curriculum.organisation=Organisation of curriculum diff --git a/src/test/java/org/olat/modules/quality/analysis/manager/StatisticsCalculatorTest.java b/src/test/java/org/olat/modules/quality/analysis/manager/StatisticsCalculatorTest.java index 5a9a83725e8f5a47b9a2799f11108586ce4613ed..510f54f7aa5bb0062524b04c800c793d3d05448b 100644 --- a/src/test/java/org/olat/modules/quality/analysis/manager/StatisticsCalculatorTest.java +++ b/src/test/java/org/olat/modules/quality/analysis/manager/StatisticsCalculatorTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; import static org.olat.test.JunitTestHelper.random; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -390,9 +391,49 @@ public class StatisticsCalculatorTest { } softly.fail("No statistic for %s, %s", multiKey, temporalKey); } + + @Test + public void shouldCalculateRubricsTotal() { + when(evaluationFormManagerMock.getRubricRating(any(), any())).thenReturn(RubricRating.NEUTRAL); + + List<Slider> sliders1 = new ArrayList<>(); + Slider slider11 = new Slider(); + String slider11Id = random(); + slider11.setId(slider11Id); + sliders1.add(slider11); + Slider slider12 = new Slider(); + String slider12Id = random(); + slider12.setId(slider12Id); + sliders1.add(slider12); + Rubric rubric1 = new Rubric(); + rubric1.setSliders(sliders1); + Rubric rubric2 = new Rubric(); + List<Slider> sliders2 = new ArrayList<>(); + Slider slider21 = new Slider(); + String slider21Id = random(); + slider21.setId(slider21Id); + slider21.setWeight(2); + sliders2.add(slider21); + rubric2.setSliders(sliders2); + List<Rubric> rubrics = Arrays.asList(rubric1, rubric2); + + List<GroupedStatistic> statistics = new ArrayList<>(); + statistics.add(new GroupedStatisticImpl(slider11Id, null, null, 1l, null, true, 1.0, null, 0)); + statistics.add(new GroupedStatisticImpl(slider12Id, null, null, 1l, null, true, 2.0, null, 0)); + statistics.add(new GroupedStatisticImpl(slider21Id, null, null, 2l, null, true, 3.0, null, 0)); + + HeatMapStatistic total = sut.calculateRubricsTotal(statistics , rubrics); + + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(total.getCount()).isEqualTo(4); + softly.assertThat(total.getAvg()).isEqualTo(2.5, offset(0.001)); + softly.assertThat(total.getRating()).isEqualTo(RubricRating.NEUTRAL); + softly.assertAll(); + } + @Test - public void shouldCalculateTotal() { + public void shouldCalculateSliderTotal() { when(evaluationFormManagerMock.getRubricRating(any(), any())).thenReturn(RubricRating.NEUTRAL); Rubric rubric = new Rubric(); @@ -402,7 +443,8 @@ public class StatisticsCalculatorTest { statistic.add(new HeatMapStatisticImpl(3l, 5.0, null)); statistic.add(new HeatMapStatisticImpl(null, null, null)); - HeatMapStatistic total = sut.calculateTotal(statistic, rubric); + HeatMapStatistic total = sut.calculateSliderTotal(statistic, rubric); + SoftAssertions softly = new SoftAssertions(); softly.assertThat(total.getCount()).isEqualTo(6); softly.assertThat(total.getAvg()).isEqualTo(3.75, offset(0.001));