From 00fdfd84ddeb3e6f9ceb85f7da8a42105002f46e Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Mon, 23 Sep 2019 12:05:17 +0200
Subject: [PATCH] OO-4207: Calculate progress of structure course nodes without
 weight

---
 .../assessment/handler/AssessmentConfig.java  |  10 +-
 .../handler/ModuleAssessmentConfig.java       |   4 +-
 .../handler/NonAssessmentConfig.java          |   4 +-
 .../manager/AssessmentHandlerRegistry.java    |  72 ++++++
 .../LearningPathOnlyAssessmentConfig.java     |   4 +-
 .../nodes/basiclti/LTIAssessmentConfig.java   |   4 +-
 .../IQIdentityListCourseNodeController.java   |   5 +-
 .../nodes/iq/IQTESTAssessmentConfig.java      |   6 +-
 .../nodes/scorm/ScormAssessmentConfig.java    |   4 +-
 .../NumberOfNodesCompletionEvaluator.java     | 110 +++++++++
 .../st/assessment/STAssessmentConfig.java     |   4 +-
 .../st/assessment/STAssessmentHandler.java    |  25 ++-
 .../nodes/survey/SurveyAssessmentConfig.java  |   4 +-
 .../run/scoring/AccountingEvaluators.java     |   2 +
 .../scoring/AccountingEvaluatorsBuilder.java  |  20 +-
 .../scoring/AccountingEvaluatorsFactory.java  |  18 +-
 .../course/run/scoring/AccountingResult.java  |  12 +
 .../run/scoring/AssessmentAccounting.java     |  10 +-
 .../run/scoring/AssessmentEvaluation.java     |   3 +-
 .../run/scoring/CompletionEvaluator.java      |  36 +++
 .../course/run/scoring/ScoreAccounting.java   |   4 +-
 .../assessment/MappedScoreAccounting.java     |  63 ++++++
 .../NumberOfNodesCompletionEvaluatorTest.java | 209 ++++++++++++++++++
 .../java/org/olat/test/AllTestsJunit4.java    |   1 +
 24 files changed, 594 insertions(+), 40 deletions(-)
 create mode 100644 src/main/java/org/olat/course/assessment/manager/AssessmentHandlerRegistry.java
 create mode 100644 src/main/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluator.java
 create mode 100644 src/main/java/org/olat/course/run/scoring/CompletionEvaluator.java
 create mode 100644 src/test/java/org/olat/course/assessment/MappedScoreAccounting.java
 create mode 100644 src/test/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluatorTest.java

diff --git a/src/main/java/org/olat/course/assessment/handler/AssessmentConfig.java b/src/main/java/org/olat/course/assessment/handler/AssessmentConfig.java
index 8394933494b..2f27a419447 100644
--- a/src/main/java/org/olat/course/assessment/handler/AssessmentConfig.java
+++ b/src/main/java/org/olat/course/assessment/handler/AssessmentConfig.java
@@ -27,6 +27,12 @@ package org.olat.course.assessment.handler;
  */
 public interface AssessmentConfig {
 	
+	public enum Mode {
+		none,
+		setByNode,
+		evaluated
+	}
+	
 	/**
 	 * Real assessments are in efficiency statements and are shown in the assessment tool.
 	 * 
@@ -75,9 +81,9 @@ public interface AssessmentConfig {
 	public Float getCutValue();
 	
 	/**
-	 * @return True if this course node can produces a completion variable for the learner
+	 * @return if this course node can produces a completion variable for the learner
 	 */
-	public boolean hasCompletion();
+	public Mode getCompletionMode();
 	
 	/**
 	 * @return True if this course node produces an attempts variable for the learner
diff --git a/src/main/java/org/olat/course/assessment/handler/ModuleAssessmentConfig.java b/src/main/java/org/olat/course/assessment/handler/ModuleAssessmentConfig.java
index ce59e6d54ca..6ad1a4bd68d 100644
--- a/src/main/java/org/olat/course/assessment/handler/ModuleAssessmentConfig.java
+++ b/src/main/java/org/olat/course/assessment/handler/ModuleAssessmentConfig.java
@@ -87,8 +87,8 @@ public abstract class ModuleAssessmentConfig implements AssessmentConfig {
 	}
 	
 	@Override
-	public boolean hasCompletion() {
-		return false;
+	public Mode getCompletionMode() {
+		return Mode.none;
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/course/assessment/handler/NonAssessmentConfig.java b/src/main/java/org/olat/course/assessment/handler/NonAssessmentConfig.java
index 4ab29e3fc64..906b9d79230 100644
--- a/src/main/java/org/olat/course/assessment/handler/NonAssessmentConfig.java
+++ b/src/main/java/org/olat/course/assessment/handler/NonAssessmentConfig.java
@@ -78,8 +78,8 @@ public class NonAssessmentConfig implements AssessmentConfig {
 	}
 
 	@Override
-	public boolean hasCompletion() {
-		return false;
+	public Mode getCompletionMode() {
+		return Mode.none;
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/course/assessment/manager/AssessmentHandlerRegistry.java b/src/main/java/org/olat/course/assessment/manager/AssessmentHandlerRegistry.java
new file mode 100644
index 00000000000..60f3de0ec33
--- /dev/null
+++ b/src/main/java/org/olat/course/assessment/manager/AssessmentHandlerRegistry.java
@@ -0,0 +1,72 @@
+/**
+ * <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.assessment.manager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.PostConstruct;
+
+import org.olat.course.assessment.handler.AssessmentHandler;
+import org.olat.course.assessment.handler.NonAssessmentHandler;
+import org.olat.course.nodes.CourseNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * Initial date: 23 Sep 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+class AssessmentHandlerRegistry {
+
+	private static final String NON_ASSESSMENT_TYPE = NonAssessmentHandler.NODE_TYPE;	
+	
+	@Autowired
+	private List<AssessmentHandler> loadedAssessmentHandlers;
+	private Map<String, AssessmentHandler> assessmentHandlers = new HashMap<>();
+	private AssessmentHandler nonAssessmentHandler;
+	
+	@PostConstruct
+	void initProviders() {
+		for (AssessmentHandler handler: loadedAssessmentHandlers) {
+			if (NON_ASSESSMENT_TYPE.equals(handler.acceptCourseNodeType())) {
+				nonAssessmentHandler = handler;
+			} else {
+				assessmentHandlers.put(handler.acceptCourseNodeType(), handler);
+			}
+		}
+	}
+
+	AssessmentHandler getAssessmentHandler(CourseNode courseNode) {
+		AssessmentHandler handler = null;
+		if (courseNode != null) {
+			handler = assessmentHandlers.get(courseNode.getType());
+		}
+		if (handler == null) {
+			handler = nonAssessmentHandler;
+		}
+		return handler;
+	}
+	
+}
diff --git a/src/main/java/org/olat/course/learningpath/LearningPathOnlyAssessmentConfig.java b/src/main/java/org/olat/course/learningpath/LearningPathOnlyAssessmentConfig.java
index 1dbd27a16a2..09ebb4a3faf 100644
--- a/src/main/java/org/olat/course/learningpath/LearningPathOnlyAssessmentConfig.java
+++ b/src/main/java/org/olat/course/learningpath/LearningPathOnlyAssessmentConfig.java
@@ -70,8 +70,8 @@ public class LearningPathOnlyAssessmentConfig implements AssessmentConfig {
 	}
 
 	@Override
-	public boolean hasCompletion() {
-		return false;
+	public Mode getCompletionMode() {
+		return Mode.none;
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/basiclti/LTIAssessmentConfig.java b/src/main/java/org/olat/course/nodes/basiclti/LTIAssessmentConfig.java
index 61a0f43b09c..95b2d8ff808 100644
--- a/src/main/java/org/olat/course/nodes/basiclti/LTIAssessmentConfig.java
+++ b/src/main/java/org/olat/course/nodes/basiclti/LTIAssessmentConfig.java
@@ -94,8 +94,8 @@ public class LTIAssessmentConfig implements AssessmentConfig {
 	}
 	
 	@Override
-	public boolean hasCompletion() {
-		return false;
+	public Mode getCompletionMode() {
+		return Mode.none;
 	}
 	
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/iq/IQIdentityListCourseNodeController.java b/src/main/java/org/olat/course/nodes/iq/IQIdentityListCourseNodeController.java
index 588ccecd7bf..23ff75409d8 100644
--- a/src/main/java/org/olat/course/nodes/iq/IQIdentityListCourseNodeController.java
+++ b/src/main/java/org/olat/course/nodes/iq/IQIdentityListCourseNodeController.java
@@ -53,6 +53,7 @@ import org.olat.course.archiver.ScoreAccountingHelper;
 import org.olat.course.assessment.AssessmentHelper;
 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.assessment.ui.tool.IdentityListCourseNodeController;
 import org.olat.course.assessment.ui.tool.IdentityListCourseNodeTableModel.IdentityCourseElementCols;
 import org.olat.course.assessment.ui.tool.IdentityListCourseNodeToolsController;
@@ -145,7 +146,7 @@ public class IQIdentityListCourseNodeController extends IdentityListCourseNodeCo
 		super.initStatusColumns(columnsModel);
 		IQTESTCourseNode testCourseNode = (IQTESTCourseNode)courseNode;
 		AssessmentConfig assessmentConfig = courseAssessmentService.getAssessmentConfig(courseNode);
-		if(testCourseNode != null && assessmentConfig.hasCompletion()) {
+		if(testCourseNode != null && Mode.setByNode.equals(assessmentConfig.getCompletionMode())) {
 			columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(IdentityCourseElementCols.currentCompletion));
 		}
 		
@@ -374,7 +375,7 @@ public class IQIdentityListCourseNodeController extends IdentityListCourseNodeCo
 					(IQTESTCourseNode)courseNode, assessedIdentity, coachCourseEnv);
 		}
 		return  new IdentityListCourseNodeToolsController(ureq, getWindowControl(),
-				(IQTESTCourseNode)courseNode, assessedIdentity, coachCourseEnv);
+				courseNode, assessedIdentity, coachCourseEnv);
 	}
 	
 	private List<Identity> getIdentities() {
diff --git a/src/main/java/org/olat/course/nodes/iq/IQTESTAssessmentConfig.java b/src/main/java/org/olat/course/nodes/iq/IQTESTAssessmentConfig.java
index 82a742134a4..733a31d2071 100644
--- a/src/main/java/org/olat/course/nodes/iq/IQTESTAssessmentConfig.java
+++ b/src/main/java/org/olat/course/nodes/iq/IQTESTAssessmentConfig.java
@@ -146,8 +146,10 @@ public class IQTESTAssessmentConfig implements AssessmentConfig {
 	}
 	
 	@Override
-	public boolean hasCompletion() {
-		return IQEditController.CONFIG_VALUE_QTI21.equals(courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE_QTI));
+	public Mode getCompletionMode() {
+		return IQEditController.CONFIG_VALUE_QTI21.equals(courseNode.getModuleConfiguration().get(IQEditController.CONFIG_KEY_TYPE_QTI))
+				? Mode.setByNode
+				: Mode.none;
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/scorm/ScormAssessmentConfig.java b/src/main/java/org/olat/course/nodes/scorm/ScormAssessmentConfig.java
index 8e1df913643..a55c9f4a354 100644
--- a/src/main/java/org/olat/course/nodes/scorm/ScormAssessmentConfig.java
+++ b/src/main/java/org/olat/course/nodes/scorm/ScormAssessmentConfig.java
@@ -86,8 +86,8 @@ public class ScormAssessmentConfig implements AssessmentConfig {
 	}
 	
 	@Override
-	public boolean hasCompletion() {
-		return false;
+	public Mode getCompletionMode() {
+		return Mode.none;
 	}
 	
 	@Override
diff --git a/src/main/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluator.java b/src/main/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluator.java
new file mode 100644
index 00000000000..6e21359cf32
--- /dev/null
+++ b/src/main/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluator.java
@@ -0,0 +1,110 @@
+/**
+ * <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 java.util.ArrayList;
+import java.util.List;
+
+import org.olat.core.util.nodes.INode;
+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.config.CourseConfig;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.run.scoring.AssessmentEvaluation;
+import org.olat.course.run.scoring.CompletionEvaluator;
+import org.olat.course.run.scoring.ScoreAccounting;
+import org.olat.modules.assessment.model.AssessmentEntryStatus;
+import org.olat.modules.assessment.model.AssessmentObligation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 
+ * Initial date: 23 Sep 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+@Component
+public class NumberOfNodesCompletionEvaluator implements CompletionEvaluator {
+	
+	@Autowired
+	private CourseAssessmentService courseAssessmentService;
+	
+	@Override
+	public Double getCompletion(AssessmentEvaluation currentEvaluation, CourseNode courseNode,
+			CourseConfig courseConfig, ScoreAccounting scoreAccounting) {
+		
+		List<CourseNode> children = new ArrayList<>();
+		collectChildren(children, courseNode);
+		
+		int count = 0;
+		double completion = 0.0;
+		for (CourseNode child: children) {
+			AssessmentEvaluation assessmentEvaluation = scoreAccounting.evalCourseNode(child);
+			if (isMandatory(assessmentEvaluation)) {
+				AssessmentConfig assessmentConfig = courseAssessmentService.getAssessmentConfig(child);
+				if (Mode.setByNode.equals(assessmentConfig.getCompletionMode())) {
+					count++;
+					completion += assessmentEvaluation.getCompletion() != null
+							? assessmentEvaluation.getCompletion().doubleValue()
+							: 0.0;
+				} else if (Mode.none.equals(assessmentConfig.getCompletionMode())) {
+					count++;
+					completion += getCompletion(assessmentEvaluation);
+				}
+			}
+		}
+		
+		return count > 0? completion / count: null;
+	}
+
+	private void collectChildren(List<CourseNode> children, CourseNode courseNode) {
+		for (int i = 0; i < courseNode.getChildCount(); i++) {
+			INode child = courseNode.getChildAt(i);
+			if (child instanceof CourseNode) {
+				CourseNode childCourseNode = (CourseNode) child;
+				children.add(childCourseNode);
+				collectChildren(children, childCourseNode);
+			}
+		}
+	}
+
+	private boolean isMandatory(AssessmentEvaluation evaluation) {
+		return evaluation.getObligation() != null && AssessmentObligation.mandatory.equals(evaluation.getObligation());
+	}
+
+	private double getCompletion(AssessmentEvaluation evaluation) {
+		if (evaluation.getFullyAssessed() != null && evaluation.getFullyAssessed().booleanValue()) return 1.0;
+		
+		AssessmentEntryStatus assessmentStatus = evaluation.getAssessmentStatus();
+		if (assessmentStatus == null) return 0.0;
+		
+		switch (assessmentStatus) {
+		case notReady: return 0.0;
+		case notStarted: return 0.0;
+		case inProgress: return 0.5;
+		case inReview: return 0.75;
+		case done: return 1.0;
+		default: return 0.0;
+		}
+	}
+
+}
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 54c6840a200..02e63938eca 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
@@ -85,8 +85,8 @@ public class STAssessmentConfig implements AssessmentConfig {
 	}
 
 	@Override
-	public boolean hasCompletion() {
-		return false;
+	public Mode getCompletionMode() {
+		return Mode.evaluated;
 	}
 
 	@Override
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 cbe18864dcc..662be236cda 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
@@ -39,7 +39,6 @@ import org.olat.course.nodes.st.STIdentityListCourseNodeController;
 import org.olat.course.run.scoring.AccountingEvaluators;
 import org.olat.course.run.scoring.AccountingEvaluatorsBuilder;
 import org.olat.course.run.scoring.AssessmentEvaluation;
-import org.olat.course.run.scoring.DurationEvaluator;
 import org.olat.course.run.scoring.FullyAssessedEvaluator;
 import org.olat.course.run.scoring.LastModificationsEvaluator;
 import org.olat.course.run.scoring.PassedEvaluator;
@@ -52,6 +51,7 @@ import org.olat.modules.assessment.AssessmentEntry;
 import org.olat.modules.assessment.ui.AssessmentToolContainer;
 import org.olat.modules.assessment.ui.AssessmentToolSecurityCallback;
 import org.olat.repository.RepositoryEntry;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 /**
@@ -63,9 +63,9 @@ import org.springframework.stereotype.Service;
 @Service
 public class STAssessmentHandler implements AssessmentHandler {
 	
+	private static final CumulatingDurationEvaluator CUMULATION_DURATION_EVALUATOR = new CumulatingDurationEvaluator();
 	private static final ScoreEvaluator CONDITION_SCORE_EVALUATOR = new ConditionScoreEvaluator();
 	private static final PassedEvaluator CONDITION_PASSED_EVALUATOR = new ConditionPassedEvaluator();
-	private static final DurationEvaluator CUMMULATING_DURATION_EVALUATOR = new CumulatingDurationEvaluator();
 	private static final StatusEvaluator SCORE_STATUS_EVALUATOR = new ScoreStatusEvaluator();
 	private static final StatusEvaluator STATUS_LEARNING_PATH_STATUS_EVALUATOR = new STLinearStatusEvaluator();
 	private static final FullyAssessedEvaluator FULLY_ASSESSED_EVALUATOR = new STFullyAssessedEvaluator();
@@ -76,14 +76,9 @@ public class STAssessmentHandler implements AssessmentHandler {
 			.withStatusEvaluator(SCORE_STATUS_EVALUATOR)
 			.withLastModificationsEvaluator(LAST_MODIFICATION_EVALUATOR)
 			.build();
-	private static final AccountingEvaluators LEARNING_PATH_EVALUATORS = AccountingEvaluatorsBuilder.builder()
-			.withDurationEvaluator(CUMMULATING_DURATION_EVALUATOR)
-			.withScoreEvaluator(CONDITION_SCORE_EVALUATOR)
-			.withPassedEvaluator(CONDITION_PASSED_EVALUATOR)
-			.withStatusEvaluator(STATUS_LEARNING_PATH_STATUS_EVALUATOR)
-			.withFullyAssessedEvaluator(FULLY_ASSESSED_EVALUATOR)
-			.withLastModificationsEvaluator(LAST_MODIFICATION_EVALUATOR)
-			.build();
+	
+	@Autowired
+	private NumberOfNodesCompletionEvaluator numberOfNodesCompletionEvaluator;
 	
 	@Override
 	public String acceptCourseNodeType() {
@@ -109,7 +104,15 @@ public class STAssessmentHandler implements AssessmentHandler {
 	@Override
 	public AccountingEvaluators getEvaluators(CourseNode courseNode, NodeAccessType nodeAccessType) {
 		if (LearningPathNodeAccessProvider.TYPE.equals(nodeAccessType.getType())) {
-			return LEARNING_PATH_EVALUATORS;
+			return AccountingEvaluatorsBuilder.builder()
+					.withDurationEvaluator(CUMULATION_DURATION_EVALUATOR)
+					.withScoreEvaluator(CONDITION_SCORE_EVALUATOR)
+					.withPassedEvaluator(CONDITION_PASSED_EVALUATOR)
+					.withCompletionEvaluator(numberOfNodesCompletionEvaluator)
+					.withStatusEvaluator(STATUS_LEARNING_PATH_STATUS_EVALUATOR)
+					.withFullyAssessedEvaluator(FULLY_ASSESSED_EVALUATOR)
+					.withLastModificationsEvaluator(LAST_MODIFICATION_EVALUATOR)
+					.build();
 		}
 		return CONVENTIONAL_EVALUATORS;
 	}
diff --git a/src/main/java/org/olat/course/nodes/survey/SurveyAssessmentConfig.java b/src/main/java/org/olat/course/nodes/survey/SurveyAssessmentConfig.java
index 137aade6610..79f40b503fb 100644
--- a/src/main/java/org/olat/course/nodes/survey/SurveyAssessmentConfig.java
+++ b/src/main/java/org/olat/course/nodes/survey/SurveyAssessmentConfig.java
@@ -30,8 +30,8 @@ import org.olat.course.learningpath.LearningPathOnlyAssessmentConfig;
 public class SurveyAssessmentConfig extends LearningPathOnlyAssessmentConfig {
 
 	@Override
-	public boolean hasCompletion() {
-		return true;
+	public Mode getCompletionMode() {
+		return Mode.setByNode;
 	}
 
 }
diff --git a/src/main/java/org/olat/course/run/scoring/AccountingEvaluators.java b/src/main/java/org/olat/course/run/scoring/AccountingEvaluators.java
index 03e0ff00c01..ff2de92bdb4 100644
--- a/src/main/java/org/olat/course/run/scoring/AccountingEvaluators.java
+++ b/src/main/java/org/olat/course/run/scoring/AccountingEvaluators.java
@@ -37,6 +37,8 @@ public interface AccountingEvaluators {
 
 	public LastModificationsEvaluator getLastModificationsEvaluator();
 
+	public CompletionEvaluator getCompletionEvaluator();
+
 	public StatusEvaluator getStatusEvaluator();
 
 	public FullyAssessedEvaluator getFullyAssessedEvaluator();
diff --git a/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsBuilder.java b/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsBuilder.java
index 0b63a5ac42a..93076e0edc2 100644
--- a/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsBuilder.java
+++ b/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsBuilder.java
@@ -32,6 +32,7 @@ public class AccountingEvaluatorsBuilder {
 			.withDurationEvaluator(AccountingEvaluatorsFactory.createNullDurationEvaluator())
 			.withScoreEvaluator(AccountingEvaluatorsFactory.createUnchangingScoreEvaluator())
 			.withPassedEvaluator(AccountingEvaluatorsFactory.createUnchangingPassedEvaluator())
+			.withCompletionEvaluator(AccountingEvaluatorsFactory.createUnchangingCompletionEvaluator())
 			.withStatusEvaluator(AccountingEvaluatorsFactory.createUnchangingStatusEvaluator())
 			.withFullyAssessedEvaluator(AccountingEvaluatorsFactory.createUnchangingFullyAssessedEvaluator())
 			.withLastModificationsEvaluator(AccountingEvaluatorsFactory.createUnchangingLastModificationsEvaluator())
@@ -41,6 +42,7 @@ public class AccountingEvaluatorsBuilder {
 	private DurationEvaluator durationEvaluator;
 	private ScoreEvaluator scoreEvaluator;
 	private PassedEvaluator passedEvaluator;
+	private CompletionEvaluator completionEvaluator;
 	private StatusEvaluator statusEvaluator;
 	private FullyAssessedEvaluator fullyAssessedEvaluator;
 	private LastModificationsEvaluator lastModificationsEvaluator;
@@ -69,6 +71,11 @@ public class AccountingEvaluatorsBuilder {
 		return this;
 	}
 	
+	public AccountingEvaluatorsBuilder withCompletionEvaluator(CompletionEvaluator completionEvaluator) {
+		this.completionEvaluator = completionEvaluator;
+		return this;
+	}
+	
 	public AccountingEvaluatorsBuilder withStatusEvaluator(StatusEvaluator statusEvaluator) {
 		this.statusEvaluator = statusEvaluator;
 		return this;
@@ -98,6 +105,9 @@ public class AccountingEvaluatorsBuilder {
 		impl.passedEvaluator = this.passedEvaluator != null
 				? this.passedEvaluator
 				: AccountingEvaluatorsFactory.createUnchangingPassedEvaluator();
+		impl.completionEvaluator = this.completionEvaluator != null
+				? this.completionEvaluator
+				: AccountingEvaluatorsFactory.createUnchangingCompletionEvaluator();
 		impl.statusEvaluator = this.statusEvaluator != null
 				? this.statusEvaluator
 				: AccountingEvaluatorsFactory.createUnchangingStatusEvaluator();
@@ -124,6 +134,7 @@ public class AccountingEvaluatorsBuilder {
 		private DurationEvaluator durationEvaluator;
 		private ScoreEvaluator scoreEvaluator;
 		private PassedEvaluator passedEvaluator;
+		private CompletionEvaluator completionEvaluator;
 		private StatusEvaluator statusEvaluator;
 		private FullyAssessedEvaluator fullyAssessedEvaluator;
 		private LastModificationsEvaluator lastModificationsEvaluator;
@@ -147,7 +158,12 @@ public class AccountingEvaluatorsBuilder {
 		public PassedEvaluator getPassedEvaluator() {
 			return passedEvaluator;
 		}
-
+		
+		@Override
+		public CompletionEvaluator getCompletionEvaluator() {
+			return completionEvaluator;
+		}
+		
 		@Override
 		public StatusEvaluator getStatusEvaluator() {
 			return statusEvaluator;
@@ -162,7 +178,7 @@ public class AccountingEvaluatorsBuilder {
 		public LastModificationsEvaluator getLastModificationsEvaluator() {
 			return lastModificationsEvaluator;
 		}
-		
+
 	}
 
 }
diff --git a/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsFactory.java b/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsFactory.java
index 6d2173313df..25b521f67a0 100644
--- a/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsFactory.java
+++ b/src/main/java/org/olat/course/run/scoring/AccountingEvaluatorsFactory.java
@@ -22,6 +22,7 @@ package org.olat.course.run.scoring;
 import java.util.List;
 
 import org.olat.course.condition.interpreter.ConditionInterpreter;
+import org.olat.course.config.CourseConfig;
 import org.olat.course.nodes.CourseNode;
 import org.olat.modules.assessment.model.AssessmentEntryStatus;
 import org.olat.modules.assessment.model.AssessmentObligation;
@@ -39,6 +40,7 @@ class AccountingEvaluatorsFactory {
 	private static final DurationEvaluator NULL_DURATION_EVALUATOR = new NullDurationEvaluator();
 	private static final ScoreEvaluator UNCHANGING_SCORE_EVALUATOR = new UnchangingScoreEvaluator();
 	private static final PassedEvaluator UNCHANGING_PASSED_EVALUATOR = new UnchangingPassedEvaluator();
+	private static final CompletionEvaluator UNCHANGING_COMPLETION_EVALUATOR = new UnchangingCompletionEvaluator();
 	private static final StatusEvaluator UNCHANGING_STATUS_EVALUATOR = new UnchangingStatusEvaluator();
 	private static final FullyAssessedEvaluator UNCHANGING_FULLY_ASSESSED_EVALUATOR = new UnchangingFullyAssessedEvaluator();
 	private static final LastModificationsEvaluator UNCHANGING_LAST_MODIFICATIONS_EVALUATOR = new UnchangingLastModificationEvaluator();
@@ -58,6 +60,10 @@ class AccountingEvaluatorsFactory {
 		return UNCHANGING_PASSED_EVALUATOR;
 	}
 	
+	public static CompletionEvaluator createUnchangingCompletionEvaluator() {
+		return UNCHANGING_COMPLETION_EVALUATOR;
+	}
+	
 	static StatusEvaluator createUnchangingStatusEvaluator() {
 		return UNCHANGING_STATUS_EVALUATOR;
 	}
@@ -126,6 +132,16 @@ class AccountingEvaluatorsFactory {
 		}
 	}
 	
+	private static class UnchangingCompletionEvaluator implements CompletionEvaluator {
+
+		@Override
+		public Double getCompletion(AssessmentEvaluation currentEvaluation, CourseNode courseNode,
+				CourseConfig courseConfig, ScoreAccounting scoureAccounting) {
+			return currentEvaluation.getCompletion();
+		}
+		
+	}
+	
 	private static class UnchangingStatusEvaluator implements StatusEvaluator {
 
 		@Override
@@ -159,5 +175,5 @@ class AccountingEvaluatorsFactory {
 		}
 		
 	}
-	
+
 }
diff --git a/src/main/java/org/olat/course/run/scoring/AccountingResult.java b/src/main/java/org/olat/course/run/scoring/AccountingResult.java
index df8dacdf488..fba8d9b6f20 100644
--- a/src/main/java/org/olat/course/run/scoring/AccountingResult.java
+++ b/src/main/java/org/olat/course/run/scoring/AccountingResult.java
@@ -38,6 +38,7 @@ public class AccountingResult extends AssessmentEvaluation {
 	private AssessmentObligation evaluatedObligation;
 	private Float evaluatedScore;
 	private Boolean evaluatedPassed;
+	private Double evaluatedCompletion;
 	private AssessmentEntryStatus evaluatedStatus;
 	private Boolean evaluatedFullyAssessed;
 	private Date evaluatedLastUserModified;
@@ -50,6 +51,7 @@ public class AccountingResult extends AssessmentEvaluation {
 		this.evaluatedObligation = origin.getObligation();
 		this.evaluatedScore = origin.getScore();
 		this.evaluatedPassed = origin.getPassed();
+		this.evaluatedCompletion = origin.getCompletion();
 		this.evaluatedStatus = origin.getAssessmentStatus();
 		this.evaluatedFullyAssessed = origin.getFullyAssessed();
 		this.evaluatedLastUserModified = origin.getLastUserModified();
@@ -92,6 +94,15 @@ public class AccountingResult extends AssessmentEvaluation {
 		this.evaluatedPassed = passed;
 	}
 
+	@Override
+	public Double getCompletion() {
+		return evaluatedCompletion;
+	}
+
+	public void setCompletion(Double completion) {
+		this.evaluatedCompletion = completion;
+	}
+
 	@Override
 	public AssessmentEntryStatus getAssessmentStatus() {
 		return evaluatedStatus;
@@ -136,6 +147,7 @@ public class AccountingResult extends AssessmentEvaluation {
 				|| !Objects.equals(origin.getFullyAssessed(), evaluatedFullyAssessed)
 				|| !Objects.equals(origin.getLastUserModified(), evaluatedLastUserModified)
 				|| !Objects.equals(origin.getLastCoachModified(), evaluatedLastCoachModified)
+				|| !Objects.equals(origin.getCompletion(), evaluatedCompletion)
 				|| !Objects.equals(origin.getAssessmentStatus(), evaluatedStatus);
 	}
 
diff --git a/src/main/java/org/olat/course/run/scoring/AssessmentAccounting.java b/src/main/java/org/olat/course/run/scoring/AssessmentAccounting.java
index 4337bc690fd..18092b15fc7 100644
--- a/src/main/java/org/olat/course/run/scoring/AssessmentAccounting.java
+++ b/src/main/java/org/olat/course/run/scoring/AssessmentAccounting.java
@@ -157,9 +157,6 @@ public class AssessmentAccounting implements ScoreAccounting {
 			result.setDuration(duration);
 		}
 		
-		//TODO uh score 1:1 übernehmen (con / lp)
-		//TODO uh passed 1:1 übernehmen (con / lp)
-		//TODO uh status. con = passed. lp = childen fully assessed
 		ScoreEvaluator scoreEvaluator = evaluators.getScoreEvaluator();
 		Float score = scoreEvaluator.getScore(result, courseNode, userCourseEnvironment.getConditionInterpreter());
 		result.setScore(score);
@@ -196,6 +193,12 @@ public class AssessmentAccounting implements ScoreAccounting {
 		result.setLastUserModified(lastModifications.getLastUserModified());
 		result.setLastCoachModified(lastModifications.getLastCoachModified());
 		
+		CompletionEvaluator completionEvaluator = evaluators.getCompletionEvaluator();
+		Double completion = completionEvaluator.getCompletion(result, courseNode,
+				userCourseEnvironment.getCourseEnvironment().getCourseConfig(),
+				this);
+		result.setCompletion(completion);
+		
 		status = statusEvaluator.getStatus(result, children);
 		result.setStatus(status);
 		
@@ -220,6 +223,7 @@ public class AssessmentAccounting implements ScoreAccounting {
 		entry.setDuration(result.getDuration());
 		entry.setLastUserModified(result.getLastUserModified());
 		entry.setLastCoachModified(result.getLastCoachModified());
+		entry.setDuration(result.getDuration());
 		entry.setAssessmentStatus(result.getAssessmentStatus());
 		entry.setFullyAssessed(result.getFullyAssessed());
 		
diff --git a/src/main/java/org/olat/course/run/scoring/AssessmentEvaluation.java b/src/main/java/org/olat/course/run/scoring/AssessmentEvaluation.java
index 315e05f9106..9fed8b4b348 100644
--- a/src/main/java/org/olat/course/run/scoring/AssessmentEvaluation.java
+++ b/src/main/java/org/olat/course/run/scoring/AssessmentEvaluation.java
@@ -22,6 +22,7 @@ package org.olat.course.run.scoring;
 import java.util.Date;
 
 import org.olat.course.assessment.handler.AssessmentConfig;
+import org.olat.course.assessment.handler.AssessmentConfig.Mode;
 import org.olat.modules.assessment.AssessmentEntry;
 import org.olat.modules.assessment.model.AssessmentEntryStatus;
 import org.olat.modules.assessment.model.AssessmentObligation;
@@ -171,7 +172,7 @@ public class AssessmentEvaluation extends ScoreEvaluation {
 		Double completion = null;
 		Double currentRunCompletion = null;
 		AssessmentRunStatus runStatus = null;
-		if(assessmentConfig.hasCompletion()) {
+		if(assessmentConfig.getCompletionMode() != null && !Mode.none.equals(assessmentConfig.getCompletionMode())) {
 			completion = entry.getCompletion();
 			currentRunCompletion = entry.getCurrentRunCompletion();
 			runStatus = entry.getCurrentRunStatus();
diff --git a/src/main/java/org/olat/course/run/scoring/CompletionEvaluator.java b/src/main/java/org/olat/course/run/scoring/CompletionEvaluator.java
new file mode 100644
index 00000000000..1a642a80a37
--- /dev/null
+++ b/src/main/java/org/olat/course/run/scoring/CompletionEvaluator.java
@@ -0,0 +1,36 @@
+/**
+ * <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.run.scoring;
+
+import org.olat.course.config.CourseConfig;
+import org.olat.course.nodes.CourseNode;
+
+/**
+ * 
+ * Initial date: 22 Sep 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public interface CompletionEvaluator {
+
+	public Double getCompletion(AssessmentEvaluation currentEvaluation, CourseNode courseNode,
+			CourseConfig courseConfig, ScoreAccounting scoreAccounting);
+
+}
diff --git a/src/main/java/org/olat/course/run/scoring/ScoreAccounting.java b/src/main/java/org/olat/course/run/scoring/ScoreAccounting.java
index 5a0f2f9c031..f6bf1c2cc34 100644
--- a/src/main/java/org/olat/course/run/scoring/ScoreAccounting.java
+++ b/src/main/java/org/olat/course/run/scoring/ScoreAccounting.java
@@ -45,9 +45,9 @@ public interface ScoreAccounting {
 
 	/**
 	 * Evaluates the course node or simply returns the evaluation from the cache.
-	 * @param cn
+	 * @param cncourseNode
 	 * @return ScoreEvaluation
 	 */
-	AssessmentEvaluation evalCourseNode(CourseNode cn);
+	AssessmentEvaluation evalCourseNode(CourseNode courseNode);
 
 }
\ No newline at end of file
diff --git a/src/test/java/org/olat/course/assessment/MappedScoreAccounting.java b/src/test/java/org/olat/course/assessment/MappedScoreAccounting.java
new file mode 100644
index 00000000000..5e28800d6c3
--- /dev/null
+++ b/src/test/java/org/olat/course/assessment/MappedScoreAccounting.java
@@ -0,0 +1,63 @@
+/**
+ * <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.assessment;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.run.scoring.AssessmentEvaluation;
+import org.olat.course.run.scoring.ScoreAccounting;
+
+/**
+ * 
+ * Initial date: 23 Sep 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class MappedScoreAccounting implements ScoreAccounting {
+	
+	private Map<CourseNode, AssessmentEvaluation> nodeToEvaluation = new HashMap<>();
+	
+	public void put(CourseNode courseNode, AssessmentEvaluation evaluation) {
+		nodeToEvaluation.put(courseNode, evaluation);
+	}
+
+	@Override
+	public void evaluateAll() {
+		//
+	}
+
+	@Override
+	public boolean evaluateAll(boolean update) {
+		return false;
+	}
+
+	@Override
+	public AssessmentEvaluation getScoreEvaluation(CourseNode courseNode) {
+		return nodeToEvaluation.get(courseNode);
+	}
+
+	@Override
+	public AssessmentEvaluation evalCourseNode(CourseNode courseNode) {
+		return nodeToEvaluation.get(courseNode);
+	}
+
+}
diff --git a/src/test/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluatorTest.java b/src/test/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluatorTest.java
new file mode 100644
index 00000000000..61a3aeda27f
--- /dev/null
+++ b/src/test/java/org/olat/course/nodes/st/assessment/NumberOfNodesCompletionEvaluatorTest.java
@@ -0,0 +1,209 @@
+/**
+ * <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.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.offset;
+import static org.mockito.Mockito.when;
+import static org.olat.modules.assessment.model.AssessmentObligation.mandatory;
+import static org.olat.modules.assessment.model.AssessmentObligation.optional;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.olat.course.assessment.CourseAssessmentService;
+import org.olat.course.assessment.MappedScoreAccounting;
+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.SPCourseNode;
+import org.olat.course.nodes.STCourseNode;
+import org.olat.course.run.scoring.AssessmentEvaluation;
+import org.olat.modules.assessment.model.AssessmentEntryStatus;
+import org.olat.modules.assessment.model.AssessmentObligation;
+
+/**
+ * 
+ * Initial date: 23 Sep 2019<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class NumberOfNodesCompletionEvaluatorTest {
+	
+	@Mock
+	private AssessmentConfig configSetByNode;
+	@Mock
+	private AssessmentConfig configEvaluated;
+	@Mock
+	private AssessmentConfig configNone;
+
+	@Mock
+	private CourseAssessmentService courseAssessmentService;
+	
+	@InjectMocks
+	private NumberOfNodesCompletionEvaluator sut;
+
+	
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+		
+		when(configEvaluated.getCompletionMode()).thenReturn(Mode.evaluated);
+		when(configSetByNode.getCompletionMode()).thenReturn(Mode.setByNode);
+		when(configNone.getCompletionMode()).thenReturn(Mode.none);
+	}
+	
+	@Test
+	public void shouldReturnNullIfItHasNoChildren() {
+		MappedScoreAccounting scoreAccounting = new MappedScoreAccounting();
+		CourseNode parent = new STCourseNode();
+		
+		Double completion = sut.getCompletion(null, parent, null, scoreAccounting);
+		
+		assertThat(completion).isNull();
+	}
+
+	@Test
+	public void shouldGetAverageCompletionOfChildren() {
+		MappedScoreAccounting scoreAccounting = new MappedScoreAccounting();
+		
+		// Parent
+		CourseNode parent = new STCourseNode();
+		// Child: uncalculated 
+		CourseNode childUncalculated = new Card2BrainCourseNode();
+		parent.addChild(childUncalculated);
+		AssessmentEvaluation childUncalculatedEvaluation = createAssessmentEvaluation(mandatory, Double.valueOf(0.5), null, null);
+		scoreAccounting.put(childUncalculated, childUncalculatedEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childUncalculated)).thenReturn(configSetByNode);
+		// Child: Calculated
+		CourseNode childCalculated = new STCourseNode();
+		parent.addChild(childCalculated);
+		AssessmentEvaluation childCalculatedEvaluation = createAssessmentEvaluation(mandatory, Double.valueOf(0.1), null, null);
+		scoreAccounting.put(childCalculated, childCalculatedEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childCalculated)).thenReturn(configEvaluated);
+		
+		// Child level 2: calculated
+		CourseNode child2Uncalculated = new SPCourseNode();
+		parent.addChild(child2Uncalculated);
+		AssessmentEvaluation child2UncalculatedEvaluation = createAssessmentEvaluation(mandatory, Double.valueOf(1.0), null, null);
+		scoreAccounting.put(child2Uncalculated, child2UncalculatedEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(child2Uncalculated)).thenReturn(configSetByNode);
+		
+		Double completion = sut.getCompletion(null, parent, null, scoreAccounting);
+		
+		assertThat(completion).isEqualTo(0.75);
+	}
+	
+	@Test
+	public void shouldOnlyRespectMandatoryEvaluations() {
+		MappedScoreAccounting scoreAccounting = new MappedScoreAccounting();
+		
+		// Parent: calculated
+		CourseNode parent = new STCourseNode();
+		// Child: mandatory
+		CourseNode childMandatory = new Card2BrainCourseNode();
+		parent.addChild(childMandatory);
+		AssessmentEvaluation childMandatoryEvaluation = createAssessmentEvaluation(mandatory, Double.valueOf(0.5), null, null);
+		scoreAccounting.put(childMandatory, childMandatoryEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childMandatory)).thenReturn(configSetByNode);
+		// Child: optional
+		CourseNode childOptional = new Card2BrainCourseNode();
+		parent.addChild(childOptional);
+		AssessmentEvaluation childOptionalEvaluation = createAssessmentEvaluation(optional, Double.valueOf(0.6), null, null);
+		scoreAccounting.put(childOptional, childOptionalEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childOptional)).thenReturn(configSetByNode);
+		// Child: no obligation
+		CourseNode childNoObligation = new Card2BrainCourseNode();
+		parent.addChild(childNoObligation);
+		AssessmentEvaluation childNoObligationEvaluation = createAssessmentEvaluation(null, Double.valueOf(0.7), null, null);
+		scoreAccounting.put(childNoObligation, childNoObligationEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childNoObligation)).thenReturn(configSetByNode);
+		
+		Double completion = sut.getCompletion(null, parent, null, scoreAccounting);
+		
+		assertThat(completion).isEqualTo(0.5);
+	}
+	
+	@Test
+	public void shouldAssumeCompletionIfTheCourseNodeDoesNotSetIt() {
+		MappedScoreAccounting scoreAccounting = new MappedScoreAccounting();
+		
+		// Parent: calculated
+		CourseNode parent = new STCourseNode();
+		// Child: fully assessed
+		CourseNode childFullyAssessed = new Card2BrainCourseNode();
+		parent.addChild(childFullyAssessed);
+		AssessmentEvaluation childFullyAssessedEvaluation = createAssessmentEvaluation(mandatory, null, null, Boolean.TRUE);
+		scoreAccounting.put(childFullyAssessed, childFullyAssessedEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childFullyAssessed)).thenReturn(configNone);
+		// Child: no status
+		CourseNode childNoStatus = new Card2BrainCourseNode();
+		parent.addChild(childNoStatus);
+		AssessmentEvaluation childNoStatusEvaluation = createAssessmentEvaluation(mandatory, null, null, null);
+		scoreAccounting.put(childNoStatus, childNoStatusEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childNoStatus)).thenReturn(configNone);
+		// Child: notReady
+		CourseNode childNotReady = new Card2BrainCourseNode();
+		parent.addChild(childNotReady);
+		AssessmentEvaluation childNotReadyEvaluation = createAssessmentEvaluation(mandatory, null, AssessmentEntryStatus.notReady, null);
+		scoreAccounting.put(childNotReady, childNotReadyEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childNotReady)).thenReturn(configNone);
+		// Child: notStarted
+		CourseNode childNotStarted = new Card2BrainCourseNode();
+		parent.addChild(childNotStarted);
+		AssessmentEvaluation childNotStartedEvaluation = createAssessmentEvaluation(mandatory, null, AssessmentEntryStatus.notStarted, null);
+		scoreAccounting.put(childNotStarted, childNotStartedEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childNotStarted)).thenReturn(configNone);
+		// Child: inProgress
+		CourseNode childInProgress = new Card2BrainCourseNode();
+		parent.addChild(childInProgress);
+		AssessmentEvaluation childInProgressEvaluation = createAssessmentEvaluation(mandatory, null, AssessmentEntryStatus.inProgress, null);
+		scoreAccounting.put(childInProgress, childInProgressEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childInProgress)).thenReturn(configNone);
+		// Child: inReview
+		CourseNode childInReview = new Card2BrainCourseNode();
+		parent.addChild(childInReview);
+		AssessmentEvaluation childInReviewEvaluation = createAssessmentEvaluation(mandatory, null, AssessmentEntryStatus.inReview, null);
+		scoreAccounting.put(childInReview, childInReviewEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childInReview)).thenReturn(configNone);
+		// Child: done
+		CourseNode childDone = new Card2BrainCourseNode();
+		parent.addChild(childDone);
+		AssessmentEvaluation childDoneEvaluation = createAssessmentEvaluation(mandatory, null, AssessmentEntryStatus.done, null);
+		scoreAccounting.put(childDone, childDoneEvaluation);
+		when(courseAssessmentService.getAssessmentConfig(childDone)).thenReturn(configNone);
+		
+		Double completion = sut.getCompletion(null, parent, null, scoreAccounting);
+		
+		double expected = (1.0 + 0.0 + 0.0 + 0.0 + 0.5 + 0.75 + 1.0) / 7;
+		assertThat(completion).isEqualTo(expected, offset(0.001));
+		
+	}
+	
+	private AssessmentEvaluation createAssessmentEvaluation(AssessmentObligation obligation, Double completion,
+			AssessmentEntryStatus status, Boolean fullyAssessed) {
+		return new AssessmentEvaluation(null, null, null, completion, status, null, fullyAssessed, null, null, null,
+				null, null, 0, null, null, null, null, obligation, null);
+	}
+
+}
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index 74b8f0d9949..98f2751b226 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -460,6 +460,7 @@ import org.junit.runners.Suite;
 	org.olat.course.learningpath.evaluation.LinearAccessEvaluator.class,
 	org.olat.course.learningpath.manager.LearningPathNodeAccessProviderTest.class,
 	org.olat.course.nodes.st.assessment.CumulatingDurationEvaluatorTest.class,
+	org.olat.course.nodes.st.assessment.NumberOfNodesCompletionEvaluatorTest.class,
 	org.olat.course.nodes.st.assessment.STFullyAssessedEvaluatorTest.class,
 	org.olat.course.nodes.st.assessment.STLastModificationsEvaluatorTest.class,
 	org.olat.course.nodes.st.assessment.STLinearStatusEvaluatorTest.class,
-- 
GitLab