From 11c479e71a42fad7b77061e2b18a36d42b0e2e0c Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Mon, 1 Jul 2019 09:53:33 +0200
Subject: [PATCH] OO-4124: Respect slider weight in trend diagram

---
 .../manager/QualityAnalysisServiceImpl.java   | 18 ++++-
 .../manager/StatisticsCalculator.java         | 74 ++++++++++++++++++
 .../model/RawGroupedStatisticImpl.java        | 14 ++--
 .../analysis/ui/GroupByController.java        |  2 +
 .../manager/StatisticsCalculatorTest.java     | 76 +++++++++++++++++++
 5 files changed, 175 insertions(+), 9 deletions(-)

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 a97a1d9c2e1..317f8846be8 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 1688a742191..e5ee93d0f0a 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 9bb08075900..7730b7617ad 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 d582c07f71d..8b1553a3196 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 f71707df827..afd55141c0e 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);
+	}
+
 
 }
-- 
GitLab