From 073690b9525bb43691c00bc9222dd540f89bd40f Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Mon, 14 Sep 2020 18:47:49 +0200
Subject: [PATCH] OO-4808: max score calculated from sections selection

---
 .../qti21/model/xml/QtiMaxScoreEstimator.java | 209 ++++++++++++++++++
 .../qti21/model/xml/QtiNodesExtractor.java    |   4 +-
 .../AssessmentTestComposerController.java     |   3 +-
 .../AssessmentTestEditorController.java       |  21 +-
 ...AssessmentTestOptionsEditorController.java |  45 +++-
 .../editor/_i18n/LocalStrings_de.properties   |   2 +
 .../editor/_i18n/LocalStrings_en.properties   |   2 +
 .../AssessmentSectionScoreCellRenderer.java   |   7 +-
 ...ntTestOverviewConfigurationController.java |  18 +-
 .../AssessmentTestOverviewDataModel.java      |   2 +-
 .../ui/editor/overview/ControlObjectRow.java  |  15 +-
 .../editor/overview/MaxScoreCellRenderer.java |  66 ++++++
 12 files changed, 360 insertions(+), 34 deletions(-)
 create mode 100644 src/main/java/org/olat/ims/qti21/model/xml/QtiMaxScoreEstimator.java
 create mode 100644 src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxScoreCellRenderer.java

diff --git a/src/main/java/org/olat/ims/qti21/model/xml/QtiMaxScoreEstimator.java b/src/main/java/org/olat/ims/qti21/model/xml/QtiMaxScoreEstimator.java
new file mode 100644
index 00000000000..dacd569c758
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/model/xml/QtiMaxScoreEstimator.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.ims.qti21.model.xml;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.DoubleAdder;
+
+import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
+import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef;
+import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection;
+import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
+import uk.ac.ed.ph.jqtiplus.node.test.SectionPart;
+import uk.ac.ed.ph.jqtiplus.node.test.Selection;
+import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
+
+/**
+ * 
+ * Initial date: 14 sept. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QtiMaxScoreEstimator {
+	
+	public static Double estimateMaxScore(ResolvedAssessmentTest resolvedAssessmentTest) {
+		AssessmentTest assessmentTest = resolvedAssessmentTest.getRootNodeLookup().extractIfSuccessful();
+		
+		MaxScoreVisitor visitor = new MaxScoreVisitor();
+		for (final TestPart testPart:assessmentTest.getTestParts()) {
+			 for (final AssessmentSection section:testPart.getAssessmentSections()) {
+				 doAssessmentSectionEstimateMaxScore(section, resolvedAssessmentTest, visitor);
+			 } 
+		}
+		return visitor.get();
+	}
+	
+	public static Double estimateMaxScore(TestPart testPart, ResolvedAssessmentTest resolvedAssessmentTest) {
+		MaxScoreVisitor visitor = new MaxScoreVisitor();
+		for(final AssessmentSection section:testPart.getAssessmentSections()) {
+			doAssessmentSectionEstimateMaxScore(section, resolvedAssessmentTest, visitor);
+		} 
+		return visitor.get();
+	}
+	
+	public static Double estimateMaxScore(AssessmentSection section, ResolvedAssessmentTest resolvedAssessmentTest) {
+		MaxScoreVisitor visitor = new MaxScoreVisitor();
+		doAssessmentSectionEstimateMaxScore(section, resolvedAssessmentTest, visitor);
+		return visitor.get();
+	}
+
+	private static void estimateMaxScore(SectionPart sectionPart, ResolvedAssessmentTest resolvedAssessmentTest, MaxScoreVisitor visitor) {
+		if(sectionPart instanceof AssessmentSection) {
+			doAssessmentSectionEstimateMaxScore((AssessmentSection)sectionPart, resolvedAssessmentTest, visitor);
+		} else if(sectionPart instanceof AssessmentItemRef) {
+			AssessmentItemRef itemRef = (AssessmentItemRef)sectionPart;
+			ResolvedAssessmentItem resolvedAssessmentItem = resolvedAssessmentTest.getResolvedAssessmentItem(itemRef);
+			AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractAssumingSuccessful();
+			if(assessmentItem != null) {
+				Double maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem);
+				visitor.add(maxScore);
+			}
+		}
+	}
+
+	private static void doAssessmentSectionEstimateMaxScore(AssessmentSection section, ResolvedAssessmentTest resolvedAssessmentTest, MaxScoreVisitor visitor) {
+		Selection selection = section.getSelection();
+		if(selection != null) {
+			selectSectionParts(section, resolvedAssessmentTest, visitor);
+		} else {
+			List<SectionPart> sectionParts = section.getSectionParts();
+			for(SectionPart sectionPart:sectionParts) {
+				estimateMaxScore(sectionPart, resolvedAssessmentTest, visitor);
+			}
+		}
+	}
+	
+	private static void selectSectionParts(AssessmentSection section, ResolvedAssessmentTest resolvedAssessmentTest, MaxScoreVisitor visitor) {
+		final List<SectionPart> children = section.getSectionParts();
+        final Selection selection = section.getSelection();
+        final int childCount = children.size();
+        int requestedSelections = selection.getSelect();
+        
+        if (requestedSelections < 0) {
+            requestedSelections = 0;
+        }
+        if (requestedSelections==0) {
+            return;
+        }
+        
+        int requiredChildCount = 0; /* (Number of children marked as required) */
+
+        final List<MaxScoreVisitor> childrenMaxScore = new ArrayList<>();
+        for(SectionPart child:children) {
+        	MaxScoreVisitor childVisitor = new MaxScoreVisitor();
+        	estimateMaxScore(child, resolvedAssessmentTest, childVisitor);
+        	childrenMaxScore.add(childVisitor);
+        }
+        final List<MaxScoreVisitor> immutableChildrenMaxScore = new ArrayList<>(childrenMaxScore);
+
+        /* Note any required selections */
+        for (int i=0; i<childCount; i++) {
+            if (children.get(i).getRequired()) {
+                requiredChildCount++;
+                visitor.add(childrenMaxScore.get(i));
+            }
+        }
+
+        if (requiredChildCount > requestedSelections) {
+        	requestedSelections = requiredChildCount;
+        }
+        
+        final int remainingSelections = requestedSelections - requiredChildCount;
+        if (remainingSelections > 0) {
+            if (selection.getWithReplacement()) {
+            	final int maxIndex = maxIndex(immutableChildrenMaxScore);
+                for (int i=0; i<remainingSelections; i++) {
+                	MaxScoreVisitor max = immutableChildrenMaxScore.get(maxIndex);
+                	visitor.add(max);
+                }
+            }
+            else {
+            	// don't use twice the required section
+            	for (int i=0; i<childCount; i++) {
+                    if (children.get(i).getRequired()) {
+                        childrenMaxScore.set(i, null);
+                    }
+                }
+            	
+                /* Selection without replacement */
+                for (int i=0; i<remainingSelections; i++) {
+                    int index = maxIndex(childrenMaxScore);
+                    MaxScoreVisitor max = childrenMaxScore.get(index);
+                    childrenMaxScore.set(index, null);
+                    visitor.add(max);
+                }
+            }
+        }
+	}
+	
+	private static int maxIndex(List<MaxScoreVisitor> visitorList) {
+		int maxIndex = -1;
+		double maxVal = -1.0d;
+		for(int i=0;i<visitorList.size(); i++) {
+			MaxScoreVisitor visitor = visitorList.get(i);
+			if(visitor != null && visitor.maxScoreTotal != null && maxVal < visitor.maxScoreTotal.doubleValue()) {
+				maxVal = visitor.maxScoreTotal.doubleValue();
+				maxIndex  = i;
+			}
+		}
+		return maxIndex;
+	}
+	
+	public static class MaxScoreVisitor implements Comparable<MaxScoreVisitor> {
+		
+		private DoubleAdder maxScoreTotal;
+		
+		public Double get() {
+			return maxScoreTotal == null ? null : maxScoreTotal.doubleValue();
+		}
+		
+		public void add(Double val) {
+			if(val == null) return;
+			
+			if(maxScoreTotal == null) {
+				maxScoreTotal = new DoubleAdder();
+			} 
+			maxScoreTotal.add(val.doubleValue());
+		}
+		
+		public void add(MaxScoreVisitor visitor) {
+			if(visitor == null || visitor.maxScoreTotal == null) return;
+			add(visitor.maxScoreTotal.doubleValue());
+		}
+
+		@Override
+		public int compareTo(MaxScoreVisitor o) {
+			if(maxScoreTotal == null && o.maxScoreTotal == null) {
+				return 0;
+			}
+			if(maxScoreTotal == null) {
+				return -1;
+			}
+			if(o.maxScoreTotal == null) {
+				return 1;
+			}
+			return Double.compare(maxScoreTotal.doubleValue(), o.maxScoreTotal.doubleValue());
+		}
+	}
+
+}
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/QtiNodesExtractor.java b/src/main/java/org/olat/ims/qti21/model/xml/QtiNodesExtractor.java
index fea7038682e..6bf2087dd77 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/QtiNodesExtractor.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/QtiNodesExtractor.java
@@ -87,7 +87,7 @@ public interface QtiNodesExtractor {
 			if(defaultValue != null) {
 				Value evaluatedValue = defaultValue.evaluate();
 				if(evaluatedValue instanceof FloatValue) {
-					doubleValue = new Double(((FloatValue)evaluatedValue).doubleValue());
+					doubleValue = Double.valueOf(((FloatValue)evaluatedValue).doubleValue());
 				}
 			}
 		}
@@ -196,7 +196,7 @@ public interface QtiNodesExtractor {
 	}
 	
 	public static Double extractCutValue(OutcomeIf outcomeIf) {
-		if(outcomeIf != null && outcomeIf.getExpressions().size() > 0) {
+		if(outcomeIf != null && !outcomeIf.getExpressions().isEmpty()) {
 			Expression gte = outcomeIf.getExpressions().get(0);
 			if(gte.getExpressions().size() > 1) {
 				Expression baseValue = gte.getExpressions().get(1);
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
index c2bf3fe1b91..0c88bea14c1 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestComposerController.java
@@ -1288,7 +1288,8 @@ public class AssessmentTestComposerController extends MainLayoutBasicController
 			File testFile = new File(testURI);
 			TestPart uniqueTestPart = test.getTestParts().size() == 1 ? test.getTestParts().get(0) : null;
 			currentEditorCtrl = new AssessmentTestEditorController(ureq, getWindowControl(), testEntry,
-					assessmentTestBuilder, uniqueTestPart, unzippedDirRoot, unzippedContRoot, testFile, restrictedEdit);
+					assessmentTestBuilder, resolvedAssessmentTest, uniqueTestPart,
+					unzippedDirRoot, unzippedContRoot, testFile, restrictedEdit);
 		} else if(uobject instanceof TestPart) {
 			currentEditorCtrl = new AssessmentTestPartEditorController(ureq, getWindowControl(), (TestPart)uobject,
 					restrictedEdit, assessmentTestBuilder.isEditable());
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java
index bc0cf3a9fa1..d5f373f6b1a 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestEditorController.java
@@ -25,7 +25,6 @@ import java.util.List;
 import org.olat.core.gui.UserRequest;
 import org.olat.core.gui.components.Component;
 import org.olat.core.gui.components.tabbedpane.TabbedPane;
-import org.olat.core.gui.components.velocity.VelocityContainer;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
@@ -43,6 +42,7 @@ import org.olat.repository.RepositoryEntry;
 
 import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
 import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
 
 /**
  * 
@@ -53,7 +53,6 @@ import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
 public class AssessmentTestEditorController extends BasicController implements Activateable2 {
 
 	private final TabbedPane tabbedPane;
-	private final VelocityContainer mainVC;
 	
 	private AssessmentTestOptionsEditorController optionsCtrl;
 	private AssessmentTestPartEditorController testPartOptionsCtrl;
@@ -68,28 +67,26 @@ public class AssessmentTestEditorController extends BasicController implements A
 	private final TestPart testPart;
 	private final AssessmentTest assessmentTest;
 	private final AssessmentTestBuilder testBuilder;
+	private final ResolvedAssessmentTest resolvedAssessmentTest;
 	
 	public AssessmentTestEditorController(UserRequest ureq, WindowControl wControl,
-			RepositoryEntry testEntry, AssessmentTestBuilder testBuilder, TestPart testPart,
-			File rootDirectory, VFSContainer rootContainer, File testFile,boolean restrictedEdit) {
+			RepositoryEntry testEntry, AssessmentTestBuilder testBuilder, ResolvedAssessmentTest resolvedAssessmentTest,
+			TestPart testPart, File rootDirectory, VFSContainer rootContainer, File testFile, boolean restrictedEdit) {
 		super(ureq, wControl, Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale()));
 		this.testEntry = testEntry;
 		this.testBuilder = testBuilder;
 		this.testPart = testPart;
 		this.assessmentTest = testBuilder.getAssessmentTest();
+		this.resolvedAssessmentTest = resolvedAssessmentTest;
 		this.testFile = testFile;
 		this.rootDirectory = rootDirectory;
 		this.rootContainer = rootContainer;
 		this.restrictedEdit = restrictedEdit;
 		
-		mainVC = createVelocityContainer("assessment_test_editor");
-		mainVC.contextPut("restrictedEdit", restrictedEdit);
 		tabbedPane = new TabbedPane("testTabs", getLocale());
 		tabbedPane.addListener(this);
-		mainVC.put("tabbedpane", tabbedPane);
-		
 		initTestEditor(ureq);
-		putInitialPanel(mainVC);
+		putInitialPanel(tabbedPane);
 	}
 	
 	@Override
@@ -99,12 +96,14 @@ public class AssessmentTestEditorController extends BasicController implements A
 	
 	private void initTestEditor(UserRequest ureq) {
 		if(testPart != null) {//combined test and single part editor
-			optionsCtrl = new AssessmentTestOptionsEditorController(ureq, getWindowControl(), testEntry, assessmentTest, testBuilder, restrictedEdit);
+			optionsCtrl = new AssessmentTestOptionsEditorController(ureq, getWindowControl(), testEntry,
+					assessmentTest, resolvedAssessmentTest, testBuilder, restrictedEdit);
 			testPartOptionsCtrl = new AssessmentTestPartEditorController(ureq, getWindowControl(), testPart, restrictedEdit, testBuilder.isEditable());
 			testPartOptionsCtrl.setFormTitle(null);
 			listenTo(testPartOptionsCtrl);
 		} else {
-			optionsCtrl = new AssessmentTestOptionsEditorController(ureq, getWindowControl(), testEntry, assessmentTest, testBuilder, restrictedEdit);
+			optionsCtrl = new AssessmentTestOptionsEditorController(ureq, getWindowControl(), testEntry,
+					assessmentTest, resolvedAssessmentTest, testBuilder, restrictedEdit);
 		}
 		listenTo(optionsCtrl);
 		
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestOptionsEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestOptionsEditorController.java
index a17c992f3e7..0f146cf6b9a 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestOptionsEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentTestOptionsEditorController.java
@@ -32,6 +32,7 @@ import org.olat.core.gui.components.form.flexible.impl.elements.FormSubmit;
 import org.olat.core.gui.components.util.KeyValues;
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.helpers.Settings;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.course.assessment.AssessmentHelper;
@@ -39,6 +40,7 @@ import org.olat.ims.qti21.QTI21DeliveryOptions;
 import org.olat.ims.qti21.QTI21DeliveryOptions.PassedType;
 import org.olat.ims.qti21.QTI21Service;
 import org.olat.ims.qti21.model.xml.AssessmentTestBuilder;
+import org.olat.ims.qti21.model.xml.QtiMaxScoreEstimator;
 import org.olat.ims.qti21.ui.AssessmentTestDisplayController;
 import org.olat.ims.qti21.ui.editor.events.AssessmentTestEvent;
 import org.olat.repository.RepositoryEntry;
@@ -46,6 +48,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 
 import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
 import uk.ac.ed.ph.jqtiplus.node.test.TimeLimits;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
 
 /**
  * 
@@ -62,23 +65,27 @@ public class AssessmentTestOptionsEditorController extends FormBasicController {
 	private MultipleSelectionElement maxTimeEl;
 	private MultipleSelectionElement passedEnabledEl;
 	private SingleSelection passedTypeEl;
-	private TextElement maxTimeHourEl, maxTimeMinuteEl;
-	private TextElement titleEl, maxScoreEl, cutValueEl;
+	private TextElement titleEl;
+	private TextElement cutValueEl;
+	private TextElement maxTimeHourEl;
+	private TextElement maxTimeMinuteEl;
 	
 	private final RepositoryEntry testEntry;
 	private final boolean restrictedEdit;
 	private final AssessmentTest assessmentTest;
 	private final AssessmentTestBuilder testBuilder;
+	private final ResolvedAssessmentTest resolvedAssessmentTest;
 	private QTI21DeliveryOptions deliveryOptions;
 	
 	@Autowired
 	private QTI21Service qti21Service;
 	
 	public AssessmentTestOptionsEditorController(UserRequest ureq, WindowControl wControl, RepositoryEntry testEntry,
-			AssessmentTest assessmentTest, AssessmentTestBuilder testBuilder, boolean restrictedEdit) {
+			AssessmentTest assessmentTest, ResolvedAssessmentTest resolvedAssessmentTest, AssessmentTestBuilder testBuilder, boolean restrictedEdit) {
 		super(ureq, wControl, Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale()));
 		this.testEntry = testEntry;
 		this.assessmentTest = assessmentTest;
+		this.resolvedAssessmentTest = resolvedAssessmentTest;
 		this.testBuilder = testBuilder;
 		this.restrictedEdit = restrictedEdit;
 		this.deliveryOptions = qti21Service.getDeliveryOptions(testEntry);
@@ -94,11 +101,15 @@ public class AssessmentTestOptionsEditorController extends FormBasicController {
 		titleEl.setMandatory(true);
 		titleEl.setEnabled(testBuilder.isEditable());
 
-		//score
-		String maxScore = testBuilder.getMaxScore() == null ? "" : AssessmentHelper.getRoundedScore(testBuilder.getMaxScore());
-		maxScoreEl = uifactory.addTextElement("max.score", "max.score", 8, maxScore, formLayout);
-		maxScoreEl.setEnabled(false);
-		
+		// max score estimated with shuffled and randomized sections, items
+		String estimatedMaxScore = getEstimatedMaxScoreText();
+		uifactory.addStaticTextElement("max.score", estimatedMaxScore, formLayout);
+		if(Settings.isDebuging()) {
+			// mostly for me
+			Double absolutMaxScore = testBuilder.getMaxScore();
+			uifactory.addStaticTextElement("absolut.max.score", AssessmentHelper.getRoundedScore(absolutMaxScore), formLayout);
+		}
+
 		Double cutValue = testBuilder.getCutValue();
 		PassedType passedType = deliveryOptions.getPassedType(cutValue);
 		
@@ -163,6 +174,18 @@ public class AssessmentTestOptionsEditorController extends FormBasicController {
 		updateUI();
 	}
 	
+	private String getEstimatedMaxScoreText() {
+		Double estimatedMaxScore = QtiMaxScoreEstimator.estimateMaxScore(resolvedAssessmentTest);
+		if(estimatedMaxScore == null) {
+			estimatedMaxScore = testBuilder.getMaxScore();
+		}
+		StringBuilder sb = new StringBuilder();
+		if(estimatedMaxScore != null) {
+			sb.append(AssessmentHelper.getRoundedScore(estimatedMaxScore));
+		}
+		return sb.toString();
+	}
+	
 	private void updateUI() {
 		boolean passedTypeVisible = passedEnabledEl.isAtLeastSelected(1);
 		passedTypeEl.setVisible(passedTypeVisible);
@@ -184,7 +207,7 @@ public class AssessmentTestOptionsEditorController extends FormBasicController {
 
 	@Override
 	protected boolean validateFormLogic(UserRequest ureq) {
-		boolean allOk = true;
+		boolean allOk = super.validateFormLogic(ureq);
 
 		titleEl.clearError();
 		if(!StringHelper.containsNonWhitespace(titleEl.getValue())) {
@@ -224,7 +247,7 @@ public class AssessmentTestOptionsEditorController extends FormBasicController {
 			allOk &= validateTime(maxTimeMinuteEl);
 		}
 		
-		return allOk & super.validateFormLogic(ureq);
+		return allOk;
 	}
 	
 	private boolean validateTime(TextElement timeEl) {
@@ -275,7 +298,7 @@ public class AssessmentTestOptionsEditorController extends FormBasicController {
 		
 		String cutValue = cutValueEl.isVisible()? cutValueEl.getValue(): null;
 		if(StringHelper.containsNonWhitespace(cutValue)) {
-			testBuilder.setCutValue(new Double(cutValue));
+			testBuilder.setCutValue(Double.valueOf(cutValue));
 		} else {
 			testBuilder.setCutValue(null);
 		}
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
index 7968a56c092..4d0a2b8518d 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
@@ -1,6 +1,7 @@
 #Mon Oct 30 09:56:22 CET 2017
 MULTIPLE=Multiple choice
 SINGLE=Single choice
+absolut.max.score=Summe von alle  maximal erreichbare Punktzahl
 add=Hinzuf\u00FCgen
 add.additional.feedback=Bedingtes Feedback hinzuf\u00FCgen
 add.answered.feedback=Feedback bei Antwort hinzuf\u00FCgen
@@ -224,6 +225,7 @@ math.operator.smallerEquals=<\=\t\t\t\t
 max.choices=Max. Anzahl m\u00F6glicher Antworten
 max.choices.unlimited=Beliebig
 max.score=Maximal erreichbare Punktzahl
+max.score.configuration.explain={0} von {1}
 min.choices=Min. Anzahl m\u00F6glicher Antworten
 min.choices.unlimited=Nicht begrenzt
 min.score=Minimal erreichbare Punktzahl
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
index 4d437c8b38f..72b36b944c0 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
@@ -1,6 +1,7 @@
 #Mon Aug 26 17:14:35 CEST 2019
 MULTIPLE=Multiple choice
 SINGLE=Single choice
+absolut.max.score=Total max. score of every items
 add=Add
 add.additional.feedback=Add feedback with conditions
 add.answered.feedback=Add feedback by answer
@@ -224,6 +225,7 @@ math.operator.smallerEquals=<\=
 max.choices=Max. number of possible answers
 max.choices.unlimited=Unlimited
 max.score=Max. score
+max.score.configuration.explain={0} of {1}
 min.choices=Min. number of possible answers
 min.choices.unlimited=Not limited
 min.score=Min. score
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentSectionScoreCellRenderer.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentSectionScoreCellRenderer.java
index e54a2c59494..4c057a344f6 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentSectionScoreCellRenderer.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentSectionScoreCellRenderer.java
@@ -26,7 +26,6 @@ import org.olat.core.gui.render.Renderer;
 import org.olat.core.gui.render.StringOutput;
 import org.olat.core.gui.render.URLBuilder;
 import org.olat.core.gui.translator.Translator;
-import org.olat.modules.assessment.ui.ScoreCellRenderer;
 
 import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection;
 import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
@@ -39,11 +38,11 @@ import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
  */
 public class AssessmentSectionScoreCellRenderer implements FlexiCellRenderer {
 	
-	private final ScoreCellRenderer scoreRenderer;
+	private final MaxScoreCellRenderer scoreRenderer;
 	private final StaticFlexiCellRenderer actionRenderer;
 	
-	public AssessmentSectionScoreCellRenderer(String action) {
-		scoreRenderer = new ScoreCellRenderer();
+	public AssessmentSectionScoreCellRenderer(String action, Translator translator) {
+		scoreRenderer = new MaxScoreCellRenderer(translator);
 		actionRenderer = new StaticFlexiCellRenderer(action, scoreRenderer);
 	}
 
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewConfigurationController.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewConfigurationController.java
index 7119ed772fa..5c00b01d27b 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewConfigurationController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewConfigurationController.java
@@ -56,6 +56,7 @@ import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.course.assessment.AssessmentHelper;
 import org.olat.ims.qti21.model.xml.ManifestBuilder;
+import org.olat.ims.qti21.model.xml.QtiMaxScoreEstimator;
 import org.olat.ims.qti21.model.xml.QtiNodesExtractor;
 import org.olat.ims.qti21.ui.assessment.components.QuestionTypeFlexiCellRenderer;
 import org.olat.ims.qti21.ui.editor.AssessmentTestComposerController;
@@ -150,7 +151,10 @@ public class AssessmentTestOverviewConfigurationController extends FormBasicCont
 		creatorEl.setEnabled(false);
 		DateChooser creationEl = uifactory.addDateChooser("form.metadata.creationDate", testEntry.getCreationDate(), infosLayout);
 		creationEl.setEnabled(false);
-		Double maxScore = QtiNodesExtractor.extractMaxScore(assessmentTest);
+		Double maxScore = QtiMaxScoreEstimator.estimateMaxScore(resolvedAssessmentTest);
+		if(maxScore == null) {
+			maxScore = QtiNodesExtractor.extractMaxScore(assessmentTest);
+		}
 		String maxScoreStr = AssessmentHelper.getRoundedScore(maxScore);
 		TextElement maxScoreEl = uifactory.addTextElement("max.score", "max.score", 255, maxScoreStr, infosLayout);
 		maxScoreEl.setEnabled(false);
@@ -198,7 +202,7 @@ public class AssessmentTestOverviewConfigurationController extends FormBasicCont
 		tableColumnModel.addFlexiColumnModel(titleCol);
 		// score
 		DefaultFlexiColumnModel scoreCol = new DefaultFlexiColumnModel(PartCols.maxScore, SelectionTarget.maxpoints.name());
-		scoreCol.setCellRenderer(new AssessmentSectionScoreCellRenderer(SelectionTarget.maxpoints.name()));
+		scoreCol.setCellRenderer(new AssessmentSectionScoreCellRenderer(SelectionTarget.maxpoints.name(), getTranslator()));
 		tableColumnModel.addFlexiColumnModel(scoreCol);
 		// typical learning time
 		tableColumnModel.addFlexiColumnModel(new DefaultFlexiColumnModel(PartCols.learningTime, new DurationFlexiCellRenderer(getTranslator())));
@@ -254,7 +258,7 @@ public class AssessmentTestOverviewConfigurationController extends FormBasicCont
 		List<ControlObjectRow> rows = new ArrayList<>();
 		
 		AssessmentTest test = resolvedAssessmentTest.getTestLookup().getRootNodeHolder().getRootNode();
-		ControlObjectRow testRow = ControlObjectRow.valueOf(test);
+		ControlObjectRow testRow = ControlObjectRow.valueOf(test, resolvedAssessmentTest);
 		rows.add(testRow);
 
 		AggregatedValues aggregatedValues = new AggregatedValues();
@@ -333,6 +337,10 @@ public class AssessmentTestOverviewConfigurationController extends FormBasicCont
 		
 		if(aggregatedValues.getMaxScore() != null) {
 			partRow.setMaxScore(aggregatedValues.getMaxScore());
+			Double estimatedMaxScore = QtiMaxScoreEstimator.estimateMaxScore(part, resolvedAssessmentTest);
+			if(estimatedMaxScore != null) {
+				partRow.setEstimatedMaxScore(estimatedMaxScore);
+			}
 		}
 		if(aggregatedValues.getLearningTime() != null) {
 			partRow.setLearningTime(aggregatedValues.getLearningTime());
@@ -361,6 +369,10 @@ public class AssessmentTestOverviewConfigurationController extends FormBasicCont
 		
 		if(aggregatedValues.getMaxScore() != null) {
 			sectionRow.setMaxScore(aggregatedValues.getMaxScore());
+			Double estimatedMaxScore = QtiMaxScoreEstimator.estimateMaxScore(section, resolvedAssessmentTest);
+			if(estimatedMaxScore != null) {
+				sectionRow.setEstimatedMaxScore(estimatedMaxScore);
+			}
 		}
 		if(aggregatedValues.getLearningTime() != null) {
 			sectionRow.setLearningTime(aggregatedValues.getLearningTime());
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewDataModel.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewDataModel.java
index fff267086e8..e722e95c871 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewDataModel.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/AssessmentTestOverviewDataModel.java
@@ -46,7 +46,7 @@ public class AssessmentTestOverviewDataModel extends DefaultFlexiTableDataModel<
 		ControlObjectRow partRow = getObject(row);
 		switch(PartCols.values()[col]) {
 			case title: return partRow;
-			case maxScore: return partRow.getMaxScore();
+			case maxScore: return partRow.getEstimatedMaxScore();
 			case attempts: return partRow.getAttemptOption();
 			case skipping: return partRow.getSkipping();
 			case comment: return partRow.getComment();
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/ControlObjectRow.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/ControlObjectRow.java
index c0389765aad..fd5210a5f00 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/overview/ControlObjectRow.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/ControlObjectRow.java
@@ -25,6 +25,7 @@ import org.olat.core.util.StringHelper;
 import org.olat.ims.qti21.model.QTI21QuestionType;
 import org.olat.ims.qti21.model.xml.ManifestBuilder;
 import org.olat.ims.qti21.model.xml.ManifestMetadataBuilder;
+import org.olat.ims.qti21.model.xml.QtiMaxScoreEstimator;
 import org.olat.ims.qti21.model.xml.QtiNodesExtractor;
 import org.olat.modules.qpool.manager.MetadataConverterHelper;
 import org.olat.modules.qpool.model.LOMDuration;
@@ -38,6 +39,7 @@ import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
 import uk.ac.ed.ph.jqtiplus.node.test.ControlObject;
 import uk.ac.ed.ph.jqtiplus.node.test.ItemSessionControl;
 import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
+import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentTest;
 
 /**
  * 
@@ -55,6 +57,7 @@ public class ControlObjectRow {
 	private String metadataIdentifier;
 	
 	private Double maxScore;
+	private Double estimatedMaxScore;
 	private Boolean feedbacks;
 	private QTI21QuestionType type;
 
@@ -73,9 +76,10 @@ public class ControlObjectRow {
 		this.iconCssClass = iconCssClass;
 	}
 	
-	public static ControlObjectRow valueOf(AssessmentTest assessmentTest) {
+	public static ControlObjectRow valueOf(AssessmentTest assessmentTest, ResolvedAssessmentTest resolvedAssessmentTest) {
 		ControlObjectRow row = new ControlObjectRow(assessmentTest.getTitle(), assessmentTest, "o_qtiassessment_icon");
 		row.maxScore = QtiNodesExtractor.extractMaxScore(assessmentTest);
+		row.estimatedMaxScore = QtiMaxScoreEstimator.estimateMaxScore(resolvedAssessmentTest);
 
 		boolean hasFeedbacks = !assessmentTest.getTestFeedbacks().isEmpty();
 		row.feedbacks = Boolean.valueOf(hasFeedbacks);
@@ -114,6 +118,7 @@ public class ControlObjectRow {
 		}
 		ControlObjectRow row = new ControlObjectRow(assessmentItem.getTitle(), itemRef, itemCssClass);
 		row.maxScore = QtiNodesExtractor.extractMaxScore(assessmentItem);
+		row.estimatedMaxScore = row.maxScore;
 		row.type = type;
 		boolean hasFeedbacks = !assessmentItem.getModalFeedbacks().isEmpty();
 		row.feedbacks = Boolean.valueOf(hasFeedbacks);
@@ -290,6 +295,14 @@ public class ControlObjectRow {
 		this.maxScore = maxScore;
 	}
 	
+	public Double getEstimatedMaxScore() {
+		return estimatedMaxScore;
+	}
+	
+	public void setEstimatedMaxScore(Double estimatedMaxScore) {
+		this.estimatedMaxScore = estimatedMaxScore;
+	}
+	
 	public QTI21QuestionType getType() {
 		return type;
 	}
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxScoreCellRenderer.java b/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxScoreCellRenderer.java
new file mode 100644
index 00000000000..25918eaedec
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/overview/MaxScoreCellRenderer.java
@@ -0,0 +1,66 @@
+/**
+ * <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.ims.qti21.ui.editor.overview;
+
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiCellRenderer;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableComponent;
+import org.olat.core.gui.render.Renderer;
+import org.olat.core.gui.render.StringOutput;
+import org.olat.core.gui.render.URLBuilder;
+import org.olat.core.gui.translator.Translator;
+import org.olat.course.assessment.AssessmentHelper;
+
+/**
+ * 
+ * Initial date: 14 sept. 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class MaxScoreCellRenderer implements FlexiCellRenderer {
+	
+	private final Translator translator;
+	
+	public MaxScoreCellRenderer(Translator translator) {
+		this.translator = translator;
+	}
+
+	@Override
+	public void render(Renderer renderer, StringOutput target, Object cellValue, int row, FlexiTableComponent source,
+			URLBuilder ubu, Translator transl) {
+		if(cellValue instanceof Double) {
+			ControlObjectRow objectRow = (ControlObjectRow)source.getFlexiTableElement().getTableDataModel().getObject(row);
+			Double estimatedMaxScore = objectRow.getEstimatedMaxScore();
+			Double totalMaxScore = objectRow.getMaxScore();
+			String estimated = AssessmentHelper.getRoundedScore(estimatedMaxScore);
+			String total = AssessmentHelper.getRoundedScore(totalMaxScore);
+			if(estimated != null && total != null) {
+				target.append(estimated);
+				if(!estimated.equals(total)) {
+					String explanation = translator.translate("max.score.configuration.explain", new String[] { estimated, total });
+					target.append(" <i class='o_icon o_icon_warn' title='").append(explanation).append("'> </i>");
+				}	
+			} else if(estimated != null) {
+				target.append(estimated);
+			} else if(total != null) {
+				target.append(total);
+			}
+		}
+	}
+}
-- 
GitLab