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 a97a1d9c2e1c7f50d58b98241547ab5643ba78e6..317f8846be8e3a41bcc3738c205ab05cfb36f708 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 @@ -321,13 +321,27 @@ public class QualityAnalysisServiceImpl implements QualityAnalysisService { if (groupBy == null || temporalGroupBy == null || rubrics == null || rubrics.size() == 0) return new MultiTrendSeries<>(); List<String> identifiers = rubrics.stream().map(Rubric::getSliders).flatMap(s -> s.stream()).map(Slider::getId).collect(toList()); - List<RawGroupedStatistic> statisticsList = filterDao.loadGroupedStatistic(searchParams, - identifiers, false, groupBy, temporalGroupBy); + List<RawGroupedStatistic> statisticsList; + if (hasWeights(rubrics)) { + statisticsList = filterDao.loadGroupedStatistic(searchParams, identifiers, true, groupBy, temporalGroupBy); + statisticsList = statisticsCalculator.reduceIdentifier(statisticsList, rubrics); + } else { + statisticsList = filterDao.loadGroupedStatistic(searchParams, identifiers, false, groupBy, temporalGroupBy); + } GroupedStatistics<RawGroupedStatistic> rawStatistics = new GroupedStatistics<>(statisticsList); GroupedStatistics<GroupedStatistic> statistics = statisticsCalculator.getGroupedStatistics(rawStatistics, rubrics); return statisticsCalculator.getTrendsByMultiKey(statistics, temporalGroupBy); } + private boolean hasWeights(Set<Rubric> rubrics) { + return rubrics.stream() + .map(Rubric::getSliders) + .flatMap(s -> s.stream()) + .filter(s -> s.getWeight().intValue() != 1) + .findAny() + .isPresent(); + } + @Override public boolean isInsufficient(Rubric rubric, Double avg) { RubricRating rating = evaluationFormManager.getRubricRating(rubric, avg); 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 1688a742191a5a5c9f8c159ac986d572b931ef35..e5ee93d0f0ab643015e6995c2360c64dbfced2a3 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 @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.apache.logging.log4j.Logger; import org.olat.core.logging.Tracing; @@ -46,6 +47,7 @@ import org.olat.modules.quality.analysis.TemporalKey; import org.olat.modules.quality.analysis.Trend; import org.olat.modules.quality.analysis.Trend.DIRECTION; import org.olat.modules.quality.analysis.model.GroupedStatisticImpl; +import org.olat.modules.quality.analysis.model.RawGroupedStatisticImpl; import org.olat.modules.quality.analysis.model.TrendImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -111,6 +113,58 @@ public class StatisticsCalculator { log.debug("Grouped statistic: " + statistic.toString()); return statistic; } + + List<RawGroupedStatistic> reduceIdentifier(List<RawGroupedStatistic> statisticsList, Set<Rubric> rubrics) { + Map<String, Integer> sliderToWeight = rubrics.stream() + .map(Rubric::getSliders) + .flatMap(s -> s.stream()) + .collect(Collectors.toMap(Slider::getId, Slider::getWeight)); + List<MultiKey> multiKeys = statisticsList.stream() + .map(RawGroupedStatistic::getMultiKey) + .distinct() + .collect(Collectors.toList()); + List<TemporalKey> temporalKeys = statisticsList.stream() + .map(RawGroupedStatistic::getTemporalKey) + .distinct() + .collect(Collectors.toList()); + + List<RawGroupedStatistic> statistics = new ArrayList<>(); + for (MultiKey multiKey: multiKeys) { + for (TemporalKey temporalKey: temporalKeys) { + List<RawGroupedStatistic> keysStatistics = filterByKey(statisticsList, multiKey, temporalKey); + CountAvg countAvg = getCountAvg(keysStatistics, sliderToWeight); + RawGroupedStatisticImpl reducedStatistic = new RawGroupedStatisticImpl(null, multiKey, temporalKey, + countAvg.getCount(), countAvg.getAvg()); + statistics.add(reducedStatistic); + } + } + return statistics; + } + + private List<RawGroupedStatistic> filterByKey(List<RawGroupedStatistic> statisticsList, MultiKey multiKey, + TemporalKey temporalKey) { + return statisticsList.stream() + .filter(s -> multiKey.equals(s.getMultiKey()) && temporalKey.equals(s.getTemporalKey())) + .collect(Collectors.toList()); + } + + private CountAvg getCountAvg(List<RawGroupedStatistic> keysStatistics, Map<String, Integer> sliderToWeight) { + long count = 0; + int sumCount = 0; + double sumValues = 0; + for (RawGroupedStatistic statistic : keysStatistics) { + long statisticCount = statistic.getCount()!= null? statistic.getCount().longValue(): 0; + count += statisticCount; + Integer weight = sliderToWeight.get(statistic.getIdentifier()); + sumCount += statisticCount * weight; + double statisticAvg = statistic.getRawAvg()!= null? statistic.getRawAvg().doubleValue(): 0; + sumValues += statisticAvg * statisticCount * weight; + } + return count > 0 + ? new CountAvg(count, sumValues / sumCount) + : new CountAvg(null, null); + } + MultiTrendSeries<String> getTrendsByIdentifiers(GroupedStatistics<GroupedStatistic> statistics, TemporalGroupBy temporalGroupBy) { Set<TemporalKey> temporalKeys = new HashSet<>(); @@ -209,5 +263,25 @@ public class StatisticsCalculator { } return direction; } + + private static class CountAvg { + + private final Long count; + private final Double avg; + + private CountAvg(Long count, Double avg) { + this.count = count; + this.avg = avg; + } + + public Long getCount() { + return count; + } + + public Double getAvg() { + return avg; + } + + } } diff --git a/src/main/java/org/olat/modules/quality/analysis/model/RawGroupedStatisticImpl.java b/src/main/java/org/olat/modules/quality/analysis/model/RawGroupedStatisticImpl.java index 9bb0807590083091ef30c4a5d251c77dfccd1075..7730b7617ad71d0bb1921cce65d1255e2ed2430b 100644 --- a/src/main/java/org/olat/modules/quality/analysis/model/RawGroupedStatisticImpl.java +++ b/src/main/java/org/olat/modules/quality/analysis/model/RawGroupedStatisticImpl.java @@ -31,19 +31,19 @@ import org.olat.modules.quality.analysis.TemporalKey; */ public class RawGroupedStatisticImpl implements RawGroupedStatistic { - private final String identitfier; + private final String identifier; private final MultiKey multiKey; private final TemporalKey temporalKey; private final Long count; private final Double rawAvg; - public RawGroupedStatisticImpl(String identitfier, String groupedKey1, String groupedKey2, String groupedKey3, + public RawGroupedStatisticImpl(String identifier, String groupedKey1, String groupedKey2, String groupedKey3, String temporalKey, Long count, Double rawAvg) { - this(identitfier, MultiKey.of(groupedKey1, groupedKey2, groupedKey3), TemporalKey.parse(temporalKey), count, rawAvg); + this(identifier, MultiKey.of(groupedKey1, groupedKey2, groupedKey3), TemporalKey.parse(temporalKey), count, rawAvg); } - public RawGroupedStatisticImpl(String identitfier, MultiKey multiKey, TemporalKey temporalKey, Long count, Double rawAvg) { - this.identitfier = identitfier; + public RawGroupedStatisticImpl(String identifier, MultiKey multiKey, TemporalKey temporalKey, Long count, Double rawAvg) { + this.identifier = identifier; this.multiKey = multiKey; this.temporalKey = temporalKey; this.count = count; @@ -52,7 +52,7 @@ public class RawGroupedStatisticImpl implements RawGroupedStatistic { @Override public String getIdentifier() { - return identitfier; + return identifier; } @Override @@ -79,7 +79,7 @@ public class RawGroupedStatisticImpl implements RawGroupedStatistic { public String toString() { StringBuilder builder = new StringBuilder(); builder.append("RawGroupedStatisticImpl [identitfier="); - builder.append(identitfier); + builder.append(identifier); builder.append(", multiKey="); builder.append(multiKey); builder.append(", temporalKey="); 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 d582c07f71df29d33c20811db9500bb6f2efbe15..8b1553a319680e1b4fece20de863d1c0e5bde794 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 @@ -211,9 +211,11 @@ public abstract class GroupByController extends FormBasicController implements F } protected Set<Rubric> getTrendRubrics() { + // All rubrics if (!StringHelper.containsNonWhitespace(rubricId)) { return getSliders().stream().map(SliderWrapper::getRubric).distinct().collect(toSet()); } + // Single rubric Set<Rubric> rubrics = new HashSet<>(); rubrics.add(getRubricById(rubricId)); return rubrics; 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 f71707df827c6662e0cc9a8f9b2a14fc64151954..afd55141c0ea142e685f5d81bf33bda05906cded 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 @@ -20,7 +20,15 @@ package org.olat.modules.quality.analysis.manager; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.offset; +import static org.olat.test.JunitTestHelper.random; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.assertj.core.api.SoftAssertions; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -29,6 +37,7 @@ import org.mockito.junit.MockitoJUnitRunner; import org.olat.modules.forms.EvaluationFormManager; import org.olat.modules.forms.model.xml.Rubric; import org.olat.modules.forms.model.xml.ScaleType; +import org.olat.modules.forms.model.xml.Slider; import org.olat.modules.quality.analysis.GroupedStatistic; import org.olat.modules.quality.analysis.GroupedStatistics; import org.olat.modules.quality.analysis.MultiKey; @@ -310,5 +319,72 @@ public class StatisticsCalculatorTest { Trend trend3 = series.getTrend(3); assertThat(trend3.getDirection()).isEqualTo(DIRECTION.UP); } + + @Test + public void shouldReduceIdentifier() { + // Rubric 1 + List<Slider> sliders1 = new ArrayList<>(); + Slider slider11 = new Slider(); + slider11.setId(random()); + slider11.setWeight(1); + sliders1.add(slider11); + Slider slider12 = new Slider(); + slider12.setId(random()); + slider12.setWeight(2); + sliders1.add(slider12); + Rubric rubric1 = new Rubric(); + rubric1.setId(random()); + rubric1.setSliders(sliders1); + // Rubric2 + List<Slider> sliders2 = new ArrayList<>(); + Slider slider21 = new Slider(); + slider21.setId(random()); + slider21.setWeight(3); + sliders2.add(slider21); + Rubric rubric2 = new Rubric(); + rubric2.setId(random()); + rubric2.setSliders(sliders2); + Set<Rubric> rubrics = new HashSet<>(); + rubrics.add(rubric1); + rubrics.add(rubric2); + // Raw statistics + MultiKey multiKey1 = MultiKey.of("1"); + MultiKey multiKey2 = MultiKey.of("2"); + TemporalKey temporalKey1 = TemporalKey.of(1); + TemporalKey temporalKey2 = TemporalKey.of(2); + List<RawGroupedStatistic> statisticsList = new ArrayList<>(); + statisticsList.add(new RawGroupedStatisticImpl(slider11.getId(), multiKey1, temporalKey1, 1l, 1.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider12.getId(), multiKey1, temporalKey1, 1l, 1.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider21.getId(), multiKey1, temporalKey1, 1l, 1.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider11.getId(), multiKey1, temporalKey2, 2l, 1.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider12.getId(), multiKey1, temporalKey2, 2l, 2.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider21.getId(), multiKey1, temporalKey2, 2l, 3.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider12.getId(), multiKey2, temporalKey1, 100l, 20.2)); + statisticsList.add(new RawGroupedStatisticImpl(slider11.getId(), multiKey2, temporalKey2, 3l, 3.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider12.getId(), multiKey2, temporalKey2, 2l, 2.0)); + statisticsList.add(new RawGroupedStatisticImpl(slider21.getId(), multiKey2, temporalKey2, 1l, 1.0)); + + List<RawGroupedStatistic> reducedStatistics = sut.reduceIdentifier(statisticsList, rubrics); + + SoftAssertions softly = new SoftAssertions(); + assertReducedIdentifier(softly, reducedStatistics, multiKey1, temporalKey1, 3l, 1); + assertReducedIdentifier(softly, reducedStatistics, multiKey1, temporalKey2, 6l, 2.33); + assertReducedIdentifier(softly, reducedStatistics, multiKey2, temporalKey1, 100l, 20.2); + assertReducedIdentifier(softly, reducedStatistics, multiKey2, temporalKey2, 6l, 2); + softly.assertAll(); + } + + private void assertReducedIdentifier(SoftAssertions softly, List<RawGroupedStatistic> statistics, + MultiKey multiKey, TemporalKey temporalKey, long expectedCount, double expectedAvg) { + for (RawGroupedStatistic statistic : statistics) { + if (multiKey.equals(statistic.getMultiKey()) && temporalKey.equals(statistic.getTemporalKey())) { + softly.assertThat(statistic.getCount()).as("Count of %s, %s is wrong", multiKey, temporalKey).isEqualTo(expectedCount); + softly.assertThat(statistic.getRawAvg()).as("Average of %s, %s is wrong", multiKey, temporalKey).isEqualTo(expectedAvg, offset(0.01)); + return; + } + } + softly.fail("No statistic for %s, %s", multiKey, temporalKey); + } + }