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