From 99c58227e83af60316f8c2135077c60455a45052 Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Fri, 20 Mar 2020 15:31:05 +0100
Subject: [PATCH] OO-4582: Calculate max points of structure nodes

---
 .../course/assessment/AssessmentHelper.java   |   4 +-
 .../editor/overview/OverviewDataModel.java    |   4 +-
 .../org/olat/course/nodes/STCourseNode.java   |   2 +-
 .../st/assessment/MaxScoreCumulator.java      | 111 +++++++++
 .../st/assessment/STAssessmentConfig.java     |  22 +-
 .../st/assessment/STAssessmentHandler.java    |   6 +-
 .../st/assessment/MaxScoreCumulatorTest.java  | 228 ++++++++++++++++++
 .../java/org/olat/test/AllTestsJunit4.java    |   2 +
 8 files changed, 367 insertions(+), 12 deletions(-)
 create mode 100644 src/main/java/org/olat/course/nodes/st/assessment/MaxScoreCumulator.java
 create mode 100644 src/test/java/org/olat/course/nodes/st/assessment/MaxScoreCumulatorTest.java

diff --git a/src/main/java/org/olat/course/assessment/AssessmentHelper.java b/src/main/java/org/olat/course/assessment/AssessmentHelper.java
index 34f8e89fb46..3fb3701ed7f 100644
--- a/src/main/java/org/olat/course/assessment/AssessmentHelper.java
+++ b/src/main/java/org/olat/course/assessment/AssessmentHelper.java
@@ -451,8 +451,8 @@ public class AssessmentHelper {
 							assessmentNodeData.setScore(score);
 							hasDisplayableUserValues = true;
 						}
-						if(Mode.setByNode == assessmentConfig.getScoreMode() || Mode.setByNode == assessmentConfig.getPassedMode()) {
-							assessmentNodeData.setMaxScore(assessmentConfig.getMaxScore());
+						assessmentNodeData.setMaxScore(assessmentConfig.getMaxScore());
+						if(Mode.setByNode == assessmentConfig.getScoreMode()) {
 							assessmentNodeData.setMinScore(assessmentConfig.getMinScore());
 						}
 					}
diff --git a/src/main/java/org/olat/course/editor/overview/OverviewDataModel.java b/src/main/java/org/olat/course/editor/overview/OverviewDataModel.java
index 95744d3b7ee..122a32dca78 100644
--- a/src/main/java/org/olat/course/editor/overview/OverviewDataModel.java
+++ b/src/main/java/org/olat/course/editor/overview/OverviewDataModel.java
@@ -76,10 +76,10 @@ public class OverviewDataModel extends DefaultFlexiTreeTableDataModel<OverviewRo
 			case score: return row.getAssessmentConfig().isAssessable()
 					? Boolean.valueOf(Mode.none != row.getAssessmentConfig().getScoreMode())
 					: null;
-			case scoreMin: return row.getAssessmentConfig().isAssessable() && Mode.none != row.getAssessmentConfig().getScoreMode()
+			case scoreMin: return row.getAssessmentConfig().isAssessable() && Mode.setByNode == row.getAssessmentConfig().getScoreMode()
 					? row.getAssessmentConfig().getMinScore()
 					: null;
-			case scoreMax: return row.getAssessmentConfig().isAssessable() && Mode.none != row.getAssessmentConfig().getScoreMode()
+			case scoreMax: return row.getAssessmentConfig().isAssessable() && Mode.setByNode == row.getAssessmentConfig().getScoreMode()
 					? row.getAssessmentConfig().getMaxScore()
 					: null;
 			case passed: return row.getAssessmentConfig().isAssessable()
diff --git a/src/main/java/org/olat/course/nodes/STCourseNode.java b/src/main/java/org/olat/course/nodes/STCourseNode.java
index 936f25ea54a..915b78faa9b 100644
--- a/src/main/java/org/olat/course/nodes/STCourseNode.java
+++ b/src/main/java/org/olat/course/nodes/STCourseNode.java
@@ -380,7 +380,7 @@ public class STCourseNode extends AbstractAccessableCourseNode {
 			
 			STCourseNode stParent = getFirstSTParent(parent);
 			if (stParent != null) {
-				boolean scoreCalculatorSupported = stParent.getModuleConfiguration().getBooleanSafe(CONFIG_SCORE_CALCULATOR_SUPPORTED);
+				boolean scoreCalculatorSupported = stParent.getModuleConfiguration().getBooleanSafe(CONFIG_SCORE_CALCULATOR_SUPPORTED, true);
 				config.setBooleanEntry(CONFIG_SCORE_CALCULATOR_SUPPORTED, scoreCalculatorSupported);
 				if (scoreCalculatorSupported) {
 					scoreCalculator = new ScoreCalculator();
diff --git a/src/main/java/org/olat/course/nodes/st/assessment/MaxScoreCumulator.java b/src/main/java/org/olat/course/nodes/st/assessment/MaxScoreCumulator.java
new file mode 100644
index 00000000000..18d4a7724b3
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/st/assessment/MaxScoreCumulator.java
@@ -0,0 +1,111 @@
+/**
+ * <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.nodes.st.assessment;
+
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.util.nodes.INode;
+import org.olat.core.util.tree.TreeVisitor;
+import org.olat.core.util.tree.Visitor;
+import org.olat.course.assessment.CourseAssessmentService;
+import org.olat.course.assessment.handler.AssessmentConfig;
+import org.olat.course.assessment.handler.AssessmentConfig.Mode;
+import org.olat.course.nodes.CourseNode;
+
+/**
+ * 
+ * Initial date: 9 Mar 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+class MaxScoreCumulator {
+	
+	public interface MaxScore {
+		
+		public Float getSum();
+		
+		public Float getMax();
+	}
+	
+	private CourseAssessmentService courseAssessmentService;
+	
+	MaxScore getMaxScore(CourseNode courseNode) {
+		return getMaxScore(courseNode, courseAssessmentService());
+	}
+	
+	MaxScore getMaxScore(CourseNode courseNode, CourseAssessmentService courseAssessmentService) {
+		ScoreVisitor visitor = new ScoreVisitor(courseNode, courseAssessmentService);
+		TreeVisitor treeVisitor = new TreeVisitor(visitor, courseNode, true);
+		treeVisitor.visitAll();
+		return visitor;
+	}
+	
+	private CourseAssessmentService courseAssessmentService() {
+		if (courseAssessmentService== null) {
+			courseAssessmentService = CoreSpringFactory.getImpl(CourseAssessmentService.class);
+		}
+		return courseAssessmentService;
+	}
+	
+	private final static class ScoreVisitor implements MaxScore, Visitor {
+		
+		private final CourseNode root;
+		private int count;
+		private float sum;
+		private float max = 0;
+		
+		private final CourseAssessmentService courseAssessmentService;
+		
+		private ScoreVisitor(CourseNode root, CourseAssessmentService courseAssessmentService) {
+			this.root = root;
+			this.courseAssessmentService = courseAssessmentService;
+		}
+		
+		@Override
+		public Float getSum() {
+			return count > 0? Float.valueOf(sum): null;
+		}
+
+		@Override
+		public Float getMax() {
+			return count > 0? Float.valueOf(max): null;
+		}
+
+		@Override
+		public void visit(INode node) {
+			if (node.getIdent().equals(root.getIdent())) return;
+			
+			if (node instanceof CourseNode) {
+				CourseNode courseNode = (CourseNode)node;
+				AssessmentConfig assessmentConfig = courseAssessmentService.getAssessmentConfig(courseNode);
+				if (Mode.setByNode == assessmentConfig.getScoreMode() && !assessmentConfig.ignoreInCourseAssessment()) {
+					Float maxScore = assessmentConfig.getMaxScore();
+					if (maxScore != null) {
+						count++;
+						sum += maxScore.floatValue();
+						if (max < maxScore.floatValue()) {
+							max = maxScore.floatValue();
+						}
+					}
+				}
+			}
+		}
+	}
+	
+}
diff --git a/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentConfig.java b/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentConfig.java
index ba8f9a99a1b..32a6f1f85b3 100644
--- a/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentConfig.java
+++ b/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentConfig.java
@@ -21,7 +21,9 @@ package org.olat.course.nodes.st.assessment;
 
 import org.olat.core.util.StringHelper;
 import org.olat.course.assessment.handler.AssessmentConfig;
+import org.olat.course.nodes.CourseNode;
 import org.olat.course.nodes.STCourseNode;
+import org.olat.course.nodes.st.assessment.MaxScoreCumulator.MaxScore;
 import org.olat.course.run.scoring.ScoreCalculator;
 import org.olat.modules.ModuleConfiguration;
 
@@ -33,14 +35,20 @@ import org.olat.modules.ModuleConfiguration;
  */
 public class STAssessmentConfig implements AssessmentConfig {
 	
+	private static final MaxScoreCumulator MAX_SCORE_CUMULATOR = new MaxScoreCumulator();
+	
+	private final CourseNode courseNode;
 	private final boolean isRoot;
 	private final ModuleConfiguration rootConfig;
 	private final ScoreCalculator scoreCalculator;
 
-	public STAssessmentConfig(boolean isRoot, ModuleConfiguration rootConfig, ScoreCalculator scoreCalculator) {
+	public STAssessmentConfig(STCourseNode courseNode, boolean isRoot, ModuleConfiguration rootConfig) {
+		this.courseNode = courseNode;
 		this.isRoot = isRoot;
 		this.rootConfig = rootConfig;
-		this.scoreCalculator = scoreCalculator;
+		this.scoreCalculator = rootConfig.getBooleanSafe(STCourseNode.CONFIG_SCORE_CALCULATOR_SUPPORTED, true)
+				? courseNode.getScoreCalculator()
+				: null;
 	}
 
 	@Override
@@ -70,6 +78,16 @@ public class STAssessmentConfig implements AssessmentConfig {
 
 	@Override
 	public Float getMaxScore() {
+		if (scoreCalculator == null && rootConfig.has(STCourseNode.CONFIG_SCORE_KEY)) {
+			MaxScore maxScore = MAX_SCORE_CUMULATOR.getMaxScore(courseNode);
+			String scoreKey = rootConfig.getStringValue(STCourseNode.CONFIG_SCORE_KEY);
+			if (STCourseNode.CONFIG_SCORE_VALUE_SUM.equals(scoreKey)) {
+				return maxScore.getSum();
+			} else if (STCourseNode.CONFIG_SCORE_VALUE_AVG.equals(scoreKey)) {
+				// max (not average) because the user was maybe only in one node assessed
+				return maxScore.getMax();
+			}
+		}
 		return null;
 	}
 
diff --git a/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentHandler.java b/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentHandler.java
index b2f1851f6e2..bd10c94a280 100644
--- a/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentHandler.java
+++ b/src/main/java/org/olat/course/nodes/st/assessment/STAssessmentHandler.java
@@ -50,7 +50,6 @@ import org.olat.course.run.scoring.LastModificationsEvaluator;
 import org.olat.course.run.scoring.ObligationEvaluator;
 import org.olat.course.run.scoring.PassedEvaluator;
 import org.olat.course.run.scoring.RootPassedEvaluator;
-import org.olat.course.run.scoring.ScoreCalculator;
 import org.olat.course.run.scoring.ScoreEvaluator;
 import org.olat.course.run.scoring.StatusEvaluator;
 import org.olat.course.run.userview.UserCourseEnvironment;
@@ -96,10 +95,7 @@ public class STAssessmentHandler implements AssessmentHandler {
 			STCourseNode stCourseNode = (STCourseNode) courseNode;
 			STCourseNode root = getRoot(courseNode);
 			boolean isRoot = courseNode.getIdent().equals(root.getIdent());
-			ScoreCalculator scoreCalclualtor = root.getModuleConfiguration().getBooleanSafe(STCourseNode.CONFIG_SCORE_CALCULATOR_SUPPORTED)
-					? stCourseNode.getScoreCalculator()
-					: null;
-			return new STAssessmentConfig(isRoot, root.getModuleConfiguration(), scoreCalclualtor);
+			return new STAssessmentConfig(stCourseNode, isRoot, root.getModuleConfiguration());
 		}
 		return NonAssessmentConfig.create();
 	}
diff --git a/src/test/java/org/olat/course/nodes/st/assessment/MaxScoreCumulatorTest.java b/src/test/java/org/olat/course/nodes/st/assessment/MaxScoreCumulatorTest.java
new file mode 100644
index 00000000000..78371967c58
--- /dev/null
+++ b/src/test/java/org/olat/course/nodes/st/assessment/MaxScoreCumulatorTest.java
@@ -0,0 +1,228 @@
+/**
+ * <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.nodes.st.assessment;
+
+import static org.mockito.Mockito.when;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.olat.course.assessment.CourseAssessmentService;
+import org.olat.course.assessment.handler.AssessmentConfig;
+import org.olat.course.assessment.handler.AssessmentConfig.Mode;
+import org.olat.course.nodes.Card2BrainCourseNode;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.nodes.STCourseNode;
+import org.olat.course.nodes.st.assessment.MaxScoreCumulator.MaxScore;
+
+/**
+ * 
+ * Initial date: 20.03.2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class MaxScoreCumulatorTest {
+
+	@Mock
+	private CourseAssessmentService courseAssessmentService;
+	
+	private MaxScoreCumulator sut = new MaxScoreCumulator();
+	
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+	}
+
+	@Test
+	public void shouldGetCumulatedMaxScore() {
+		CourseNode parent = new STCourseNode();
+		
+		CourseNode child1 = new Card2BrainCourseNode();
+		parent.addChild(child1);
+		AssessmentConfig child1Config = new TestAssessmentConfig(Mode.setByNode, Float.valueOf(10), false);
+		when(courseAssessmentService.getAssessmentConfig(child1)).thenReturn(child1Config);
+		
+		CourseNode child2 = new Card2BrainCourseNode();
+		parent.addChild(child2);
+		AssessmentConfig child2Config = new TestAssessmentConfig(Mode.setByNode, Float.valueOf(20), false);
+		when(courseAssessmentService.getAssessmentConfig(child2)).thenReturn(child2Config);
+		
+		CourseNode child3 = new Card2BrainCourseNode();
+		parent.addChild(child3);
+		AssessmentConfig child3Config = new TestAssessmentConfig(Mode.setByNode, Float.valueOf(5), true);
+		when(courseAssessmentService.getAssessmentConfig(child3)).thenReturn(child3Config);
+		
+		CourseNode child4 = new Card2BrainCourseNode();
+		parent.addChild(child4);
+		AssessmentConfig child4Config = new TestAssessmentConfig(Mode.setByNode, null, false);
+		when(courseAssessmentService.getAssessmentConfig(child4)).thenReturn(child4Config);
+		
+		CourseNode child5 = new Card2BrainCourseNode();
+		parent.addChild(child5);
+		AssessmentConfig child5Config = new TestAssessmentConfig(Mode.none, Float.valueOf(10), false);
+		when(courseAssessmentService.getAssessmentConfig(child5)).thenReturn(child5Config);
+		
+		CourseNode child6 = new Card2BrainCourseNode();
+		parent.addChild(child6);
+		AssessmentConfig child6Config = new TestAssessmentConfig(Mode.evaluated, Float.valueOf(10), false);
+		when(courseAssessmentService.getAssessmentConfig(child6)).thenReturn(child6Config);
+		
+		CourseNode child11 = new Card2BrainCourseNode();
+		child6.addChild(child11);
+		AssessmentConfig child11Config = new TestAssessmentConfig(Mode.setByNode, Float.valueOf(50), false);
+		when(courseAssessmentService.getAssessmentConfig(child11)).thenReturn(child11Config);
+		
+		MaxScore maxScore = sut.getMaxScore(parent, courseAssessmentService);
+		
+		SoftAssertions softly = new SoftAssertions();
+		softly.assertThat(maxScore.getSum()).isEqualTo(80);
+		softly.assertThat(maxScore.getMax()).isEqualTo(50);
+		softly.assertAll();
+	}
+	
+	@Test
+	public void shouldReturnNullIfNoChildrenWithScore() {
+		CourseNode parent = new STCourseNode();
+		
+		CourseNode child1 = new Card2BrainCourseNode();
+		parent.addChild(child1);
+		AssessmentConfig child1Config = new TestAssessmentConfig(Mode.evaluated, Float.valueOf(10), false);
+		when(courseAssessmentService.getAssessmentConfig(child1)).thenReturn(child1Config);
+		
+		MaxScore maxScore = sut.getMaxScore(parent, courseAssessmentService);
+		
+		SoftAssertions softly = new SoftAssertions();
+		softly.assertThat(maxScore.getSum()).isNull();
+		softly.assertThat(maxScore.getMax()).isNull();
+		softly.assertAll();
+	}
+	
+	private static final class TestAssessmentConfig implements AssessmentConfig {
+
+		private final Mode scoreMode;
+		private final Float maxScore;
+		private boolean ignoreInCourseAssessment;
+
+		public TestAssessmentConfig(Mode scoreMode, Float maxScore, boolean ignoreInCourseAssessment) {
+			this.scoreMode = scoreMode;
+			this.maxScore = maxScore;
+			this.ignoreInCourseAssessment = ignoreInCourseAssessment;
+		}
+
+		@Override
+		public boolean isAssessable() {
+			return false;
+		}
+
+		@Override
+		public boolean ignoreInCourseAssessment() {
+			return ignoreInCourseAssessment;
+		}
+
+		@Override
+		public void setIgnoreInCourseAssessment(boolean ignoreInCourseAssessment) {
+			this.ignoreInCourseAssessment = ignoreInCourseAssessment;
+		}
+
+		@Override
+		public Mode getScoreMode() {
+			return scoreMode;
+		}
+
+		@Override
+		public Float getMaxScore() {
+			return maxScore;
+		}
+
+		@Override
+		public Float getMinScore() {
+			return null;
+		}
+
+		@Override
+		public Mode getPassedMode() {
+			return null;
+		}
+
+		@Override
+		public Float getCutValue() {
+			return null;
+		}
+
+		@Override
+		public Mode getCompletionMode() {
+			return null;
+		}
+
+		@Override
+		public boolean hasAttempts() {
+			return false;
+		}
+
+		@Override
+		public boolean hasComment() {
+			return false;
+		}
+
+		@Override
+		public boolean hasIndividualAsssessmentDocuments() {
+			return false;
+		}
+
+		@Override
+		public boolean hasStatus() {
+			return false;
+		}
+
+		@Override
+		public boolean isAssessedBusinessGroups() {
+			return false;
+		}
+
+		@Override
+		public boolean isEditable() {
+			return false;
+		}
+
+		@Override
+		public boolean isBulkEditable() {
+			return false;
+		}
+
+		@Override
+		public boolean hasEditableDetails() {
+			return false;
+		}
+
+		@Override
+		public boolean isExternalGrading() {
+			return false;
+		}
+
+		@Override
+		public boolean isObligationOverridable() {
+			return false;
+		}
+
+	}
+
+}
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index 9227622f1f7..5ba58a5d244 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -487,8 +487,10 @@ import org.junit.runners.Suite;
 	org.olat.course.learningpath.manager.LearningPathNodeAccessProviderTest.class,
 	org.olat.course.nodes.st.assessment.PassCounterTest.class,
 	org.olat.course.nodes.st.assessment.CumulatingDurationEvaluatorTest.class,
+	org.olat.course.nodes.st.assessment.CumulatingScoreEvaluatorTest.class,
 	org.olat.course.nodes.st.assessment.ConventionalSTCompletionEvaluatorTest.class,
 	org.olat.course.nodes.st.assessment.MandatoryObligationEvaluatorTest.class,
+	org.olat.course.nodes.st.assessment.MaxScoreCumulatorTest.class,
 	org.olat.course.nodes.st.assessment.STFullyAssessedEvaluatorTest.class,
 	org.olat.course.nodes.st.assessment.STLastModificationsEvaluatorTest.class,
 	org.olat.course.nodes.st.assessment.STRootPassedEvaluatorTest.class,
-- 
GitLab