diff --git a/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java b/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java index 2332b5447855e5115501eadc72877119ef8deff1..3ffbd6412f1087639767de82ba1f682ca1a1cd6f 100644 --- a/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java +++ b/src/main/java/org/olat/course/condition/interpreter/ConditionInterpreter.java @@ -34,6 +34,7 @@ import org.olat.core.logging.OLATRuntimeException; import org.olat.core.logging.Tracing; import org.olat.core.util.Util; import org.olat.course.condition.Condition; +import org.olat.course.condition.interpreter.score.GetAverageScoreFunction; import org.olat.course.condition.interpreter.score.GetPassedFunction; import org.olat.course.condition.interpreter.score.GetPassedWithCourseIdFunction; import org.olat.course.condition.interpreter.score.GetScoreFunction; @@ -157,6 +158,7 @@ public class ConditionInterpreter { // functions to calculate score env.addFunction(GetPassedFunction.name, new GetPassedFunction(userCourseEnv)); env.addFunction(GetScoreFunction.name, new GetScoreFunction(userCourseEnv)); + env.addFunction(GetAverageScoreFunction.NAME, new GetAverageScoreFunction(userCourseEnv)); env.addFunction(GetPassedWithCourseIdFunction.name, new GetPassedWithCourseIdFunction(userCourseEnv)); env.addFunction(GetScoreWithCourseIdFunction.name, new GetScoreWithCourseIdFunction(userCourseEnv)); diff --git a/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java b/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java index e30d8d51a724334bd987164813f2a0affe5e7d89..38b27a11c18a8ff527dfddda451547f5bdd17d67 100644 --- a/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java +++ b/src/main/java/org/olat/course/condition/interpreter/OnlyGroupConditionInterpreter.java @@ -25,6 +25,7 @@ package org.olat.course.condition.interpreter; +import org.olat.course.condition.interpreter.score.GetAverageScoreFunction; import org.olat.course.condition.interpreter.score.GetPassedFunction; import org.olat.course.condition.interpreter.score.GetPassedWithCourseIdFunction; import org.olat.course.condition.interpreter.score.GetScoreFunction; @@ -111,6 +112,7 @@ public class OnlyGroupConditionInterpreter extends ConditionInterpreter{ // functions to calculate score env.addFunction(GetPassedFunction.name, new DummyBooleanFunction(userCourseEnv)); env.addFunction(GetScoreFunction.name, new DummyDoubleFunction(userCourseEnv)); + env.addFunction(GetAverageScoreFunction.NAME, new DummyDoubleFunction(userCourseEnv)); env.addFunction(GetPassedWithCourseIdFunction.name, new DummyBooleanFunction(userCourseEnv)); env.addFunction(GetScoreWithCourseIdFunction.name, new DummyDoubleFunction(userCourseEnv)); @@ -135,11 +137,13 @@ class DummyBooleanFunction extends AbstractFunction { super(userCourseEnv); } + @Override public Object call(Object[] inStack) { // return allways true, because it is a dummy implementation without condition return ConditionInterpreter.INT_TRUE; } + @Override protected Object defaultValue() { return ConditionInterpreter.INT_TRUE; } @@ -152,11 +156,13 @@ class DummyDateFunction extends AbstractFunction { super(userCourseEnv); } + @Override public Object call(Object[] inStack) { // return allways true, because it is a dummy implementation without condition return new Double(0); } + @Override protected Object defaultValue() { return new Double(0); } @@ -168,10 +174,12 @@ class DummyDoubleFunction extends AbstractFunction { super(userCourseEnv); } + @Override public Object call(Object[] inStack) { return Double.MIN_VALUE; } + @Override protected Object defaultValue() { return Double.MIN_VALUE; } @@ -183,10 +191,12 @@ class DummyIntegerFunction extends AbstractFunction { super(userCourseEnv); } + @Override public Object call(Object[] inStack) { return Integer.MIN_VALUE; } + @Override protected Object defaultValue() { return Integer.MIN_VALUE; } @@ -198,10 +208,12 @@ class DummyStringFunction extends AbstractFunction { super(userCourseEnv); } + @Override public Object call(Object[] inStack) { return ""; } + @Override protected Object defaultValue() { return ""; } @@ -222,6 +234,7 @@ class DummyVariable extends AbstractVariable { /** * @see com.neemsoft.jmep.VariableCB#getValue() */ + @Override public Object getValue() { return new Double(0); } diff --git a/src/main/java/org/olat/course/condition/interpreter/score/GetAverageScoreFunction.java b/src/main/java/org/olat/course/condition/interpreter/score/GetAverageScoreFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..c3b0386036395cafe4cdd670306078862d994cf3 --- /dev/null +++ b/src/main/java/org/olat/course/condition/interpreter/score/GetAverageScoreFunction.java @@ -0,0 +1,101 @@ +/** + * <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.condition.interpreter.score; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.olat.course.condition.interpreter.AbstractFunction; +import org.olat.course.condition.interpreter.ArgumentParseException; +import org.olat.course.editor.CourseEditorEnv; +import org.olat.course.nodes.STCourseNode; +import org.olat.course.run.scoring.ScoreAccounting; +import org.olat.course.run.userview.UserCourseEnvironment; + +/** + * + * Initial date: 29 Jul 2019<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +public class GetAverageScoreFunction extends AbstractFunction { + + public static final String NAME = "getAverageScore"; + + public GetAverageScoreFunction(UserCourseEnvironment userCourseEnv) { + super(userCourseEnv); + } + + @Override + public Object call(Object[] inStack) { + if (inStack.length < 1) { + return handleException(new ArgumentParseException(ArgumentParseException.NEEDS_MORE_ARGUMENTS, NAME, "", + "error.moreargs", "solution.provideone.nodereference")); + } + + for (Object object : inStack) { + if (!(object instanceof String)) { + return handleException(new ArgumentParseException(ArgumentParseException.WRONG_ARGUMENT_FORMAT, NAME, "", + "error.argtype.coursnodeidexpeted", "solution.example.node.infunction")); + } + } + + List<String> childIds = Arrays.stream(inStack) + .map(o -> (String) o) + .collect(Collectors.toList()); + + // Editor mode + CourseEditorEnv cev = getUserCourseEnv().getCourseEditorEnv(); + if (cev != null) { + for (String childId: childIds) { + if (!cev.existsNode(childId)) { + return handleException(new ArgumentParseException(ArgumentParseException.REFERENCE_NOT_FOUND, NAME, + childId, "error.notfound.coursenodeid", "solution.copypastenodeid")); + } + if (!cev.isAssessable(childId)) { + return handleException(new ArgumentParseException(ArgumentParseException.REFERENCE_NOT_FOUND, NAME, + childId, "error.notassessable.coursenodid", "solution.takeassessablenode")); + } + + // Remember the reference to the node id for this condition for cycle testing. + // Allow testing against own score (self-referencing) except for ST + // course nodes as score is calculated on these node. Do not allow + // dependencies to parents as they create cycles. + if (!childId.equals(cev.getCurrentCourseNodeId()) || cev.getNode(cev.getCurrentCourseNodeId()) instanceof STCourseNode) { + cev.addSoftReference("courseNodeId", childId, true); + } + } + // return a valid value to continue with condition evaluation test + return defaultValue(); + } + + // Runtime mode + ScoreAccounting sa = getUserCourseEnv().getScoreAccounting(); + Float score = sa.evalAverageScore(childIds); + return new Double(score); + } + + @Override + protected Object defaultValue() { + return new Double(Double.MIN_VALUE); + } + +} diff --git a/src/main/java/org/olat/course/nodes/st/EditScoreCalculationEasyForm.java b/src/main/java/org/olat/course/nodes/st/EditScoreCalculationEasyForm.java index 65e035273b29500c6777055303165852f7ff4d5e..2bc4635fe0ec4ed0a3249c6df4fbe8edf799a8ee 100644 --- a/src/main/java/org/olat/course/nodes/st/EditScoreCalculationEasyForm.java +++ b/src/main/java/org/olat/course/nodes/st/EditScoreCalculationEasyForm.java @@ -58,7 +58,7 @@ import org.olat.course.run.scoring.ScoreCalculator; public class EditScoreCalculationEasyForm extends FormBasicController { private MultipleSelectionElement hasScore, hasPassed; - private SingleSelection passedType, failedType; + private SingleSelection scoreType, passedType, failedType; private MultipleSelectionElement scoreNodeIdents, passedNodeIdents; private IntegerElement passedCutValue; private ScoreCalculator sc; @@ -90,6 +90,24 @@ public class EditScoreCalculationEasyForm extends FormBasicController { hasScore.addActionListener(FormEvent.ONCLICK); hasScore.setElementCssClass("o_sel_has_score"); + String[] scoreTypeKeys = new String[] { + ScoreCalculator.SCORE_TYPE_SUM, + ScoreCalculator.SCORE_TYPE_AVG + }; + String[] scoreTypeValues = new String[] { + translate("scform.scoretype.sum"), + translate("scform.scoretype.avg") + }; + + scoreType = uifactory.addRadiosHorizontal("scoreType", null, formLayout, scoreTypeKeys, scoreTypeValues); + scoreType.setVisible(hasScore.isSelected(0)); + if (sc != null && sc.getScoreType() != null && !sc.getScoreType().equals(ScoreCalculator.SCORE_TYPE_NONE)) { + scoreType.select(sc.getScoreType(), true); + } else { + scoreType.select(ScoreCalculator.SCORE_TYPE_SUM, true); + } + scoreType.addActionListener(FormEvent.ONCLICK); + List<String> sumOfScoreNodes = (sc == null ? null : sc.getSumOfScoreNodes()); scoreNodeIdents = initNodeSelectionElement(formLayout, "scform.scoreNodeIndents", sc, sumOfScoreNodes, nodeIdentList); scoreNodeIdents.setVisible(hasScore.isSelected(0)); @@ -117,7 +135,7 @@ public class EditScoreCalculationEasyForm extends FormBasicController { } else { passedType.select(ScoreCalculator.PASSED_TYPE_CUTVALUE, true); } - passedType.addActionListener(FormEvent.ONCLICK); // Radios/Checkboxes need onclick because of IE bug OLAT-5753 + passedType.addActionListener(FormEvent.ONCLICK); int cutinitval = 0; if (sc != null) cutinitval = sc.getPassedCutValue(); @@ -248,7 +266,7 @@ public class EditScoreCalculationEasyForm extends FormBasicController { scoreNodeIdents.setErrorKey("scform.deletedNode.error", null); rv = false; } else { - scoreNodeIdents.clearError(); + scoreNodeIdents.clearError(); } } @@ -274,6 +292,7 @@ public class EditScoreCalculationEasyForm extends FormBasicController { } private void updateUI() { + scoreType.setVisible(hasScore.isSelected(0)); scoreNodeIdents.setVisible(hasScore.isSelected(0)); if (!scoreNodeIdents.isVisible()) { scoreNodeIdents.clearError(); @@ -302,9 +321,14 @@ public class EditScoreCalculationEasyForm extends FormBasicController { // 1) score configuration if (hasScore.isSelected(0)) { + String scoreTypeSelection = scoreType.isOneSelected() + ? scoreType.getSelectedKey() + : ScoreCalculator.SCORE_TYPE_SUM; + sc.setScoreType(scoreTypeSelection); sc.setSumOfScoreNodes(new ArrayList<>(scoreNodeIdents.getSelectedKeys())); - }else { + } else { //reset + sc.setScoreType(ScoreCalculator.SCORE_TYPE_NONE); sc.setSumOfScoreNodes(null); } diff --git a/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_de.properties index 6cd0ff5bb76d3d9bde32a1aefdb075be4efa932c..665899cd45272ce5810c5f6f9d7ace67d9b57ed4 100644 --- a/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_de.properties @@ -49,8 +49,10 @@ scform.passedType.error=Die Option "Punkte berechnen" muss aktiviert sein, um ei scform.passedtype=Bestanden berechnen? scform.passedtype.cutvalue=Aus Punkteminimum scform.passedtype.inherit=Von Bausteinen \u00FCbernehmen -scform.scoreNodeIndents=Punktesumme von +scform.scoreNodeIndents=Punkte von scform.scoreNodeIndents.error=Mindestens ein Baustein muss angew\u00E4hlt sein, von dem die Punkte \u00FCbernommen werden sollen. +scform.scoretype.avg=Durchschnitt +scform.scoretype.sum=Summe score.fieldset.title=Zusammengefasste Bewertung score.noinfo=Keine Angabe score.title=Punkte diff --git a/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_en.properties index c75c83a3d2ad7386a3dbe03e6b56be12b3c33018..d3c6b5810be12035800df3155ea364645cc48c7f 100644 --- a/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/course/nodes/st/_i18n/LocalStrings_en.properties @@ -51,6 +51,8 @@ scform.passedtype.cutvalue=As of minimum score scform.passedtype.inherit=Adopt from course element scform.scoreNodeIndents=Total score of scform.scoreNodeIndents.error=At least one element has to be selected from which the score shall be used. +scform.scoretype.avg=Average +scform.scoretype.sum=Sum score.fieldset.title=Combined assessment score.noinfo=Not available score.title=Score 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 ff52cab378c8e09c602d945169c09c1e45f34644..d7bd69b7a67931fa7202eefacce734989dedda0c 100644 --- a/src/main/java/org/olat/course/run/scoring/ScoreAccounting.java +++ b/src/main/java/org/olat/course/run/scoring/ScoreAccounting.java @@ -26,15 +26,16 @@ package org.olat.course.run.scoring; import java.math.BigDecimal; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.logging.log4j.Logger; import org.hibernate.LazyInitializationException; import org.olat.core.CoreSpringFactory; import org.olat.core.id.Identity; -import org.apache.logging.log4j.Logger; import org.olat.core.logging.Tracing; import org.olat.core.util.nodes.INode; import org.olat.core.util.tree.TreeVisitor; @@ -385,7 +386,7 @@ public class ScoreAccounting { /** * Evaluate the score of the course element. The method * takes the visibility of the results in account and will - * return 0.0 if the results are not visiblity. + * return 0.0 if the results are not visible. * * @param childId The specified course element ident * @return A float (never null) @@ -415,6 +416,40 @@ public class ScoreAccounting { return score; } + + /** + * Evaluate the average score of the course element. The method + * takes the visibility of the results in account. + * + * @param childIds The specified course element idents + * @return A float (never null) + */ + public Float evalAverageScore(Collection<String> childIds) { + int count = 0; + float sum = 0.0f; + + for (String childId : childIds) { + CourseNode foundNode = findChildByID(childId); + Float score = null; + if (foundNode instanceof AssessableCourseNode) { + AssessableCourseNode acn = (AssessableCourseNode) foundNode; + ScoreEvaluation se = evalCourseNode(acn); + if(se != null) { + // the node could not provide any sensible information on scoring. e.g. a STNode with no calculating rules + if(se.getUserVisible() == null || se.getUserVisible().booleanValue()) { + score = se.getScore(); + if (score != null) { + count++; + sum += score.floatValue(); + } + } + } + } + } + + // Calculate the average only if at least one score is available. + return count > 0? Float.valueOf(sum / count): Float.valueOf(0.0f); + } /** * Evaluate the passed / failed state of a course element. The method diff --git a/src/main/java/org/olat/course/run/scoring/ScoreCalculator.java b/src/main/java/org/olat/course/run/scoring/ScoreCalculator.java index 6ae1243b916ac2b0192a873c2c03c626a6bf41b9..2fdbc701b630ebcfdb59c71416e5c857fc20b6e1 100644 --- a/src/main/java/org/olat/course/run/scoring/ScoreCalculator.java +++ b/src/main/java/org/olat/course/run/scoring/ScoreCalculator.java @@ -25,10 +25,14 @@ package org.olat.course.run.scoring; +import static java.util.stream.Collectors.joining; + import java.io.Serializable; import java.util.Iterator; import java.util.List; +import org.olat.course.condition.interpreter.score.GetAverageScoreFunction; + /** * Description:<br> * The score calculator stores the expression which is used to calculate a users @@ -43,6 +47,10 @@ public class ScoreCalculator implements Serializable { private String passedExpression; private String failedExpression; + public static final String SCORE_TYPE_NONE = "no"; + public static final String SCORE_TYPE_SUM = "sum"; + public static final String SCORE_TYPE_AVG = "avg"; + /** config flag: no passed configured **/ public static final String PASSED_TYPE_NONE = "no"; /** config flag: passed based on cutvalue **/ @@ -53,6 +61,9 @@ public class ScoreCalculator implements Serializable { private boolean expertMode = false; // easy mode variables // score configuration + private String scoreType; + // nodes for all scoreTypes (not only sum) + // Can't rename because of the XML serialization private List<String> sumOfScoreNodes; // passed configuration private String passedType; @@ -66,11 +77,11 @@ public class ScoreCalculator implements Serializable { } /** - * @return Returns the passedExpression. if null, then there is no expression to calculate + * @return Returns the passedExpression. If null, then there is no expression to calculate. */ public String getPassedExpression() { // always return expression, even if in easy mode! whenever something in the easy mode - // hase been changed the one who changes something must also set the passedExpression + // has been changed the one who changes something must also set the passedExpression // to the new correct value using something like // sc.setScoreExpression(sc.getScoreExpressionFromEasyModeConfiguration()); return passedExpression; @@ -81,7 +92,7 @@ public class ScoreCalculator implements Serializable { */ public String getScoreExpression() { // always return expression, even if in easy mode! whenever something in the easy mode - // hase been changed the one who changes something must also set the passedExpression + // has been changed the one who changes something must also set the passedExpression // to the new correct value using something like // sc.setScoreExpression(sc.getScoreExpressionFromEasyModeConfiguration()); return scoreExpression; @@ -94,37 +105,50 @@ public class ScoreCalculator implements Serializable { /** * Calculate the score expression based on the easy mode configuration. This must not be used - * during calcualtion of a score but after changeing an expression in the editor to set the - * new score expression - * @return String + * during calculation of a score but after changing an expression in the editor to set the + * new score expression. + * + * @return */ public String getScoreExpressionFromEasyModeConfiguration() { - StringBuilder sb = new StringBuilder(); - if (getSumOfScoreNodes() != null && getSumOfScoreNodes().size() > 0) { - sb.append("("); - for(Iterator<String> iter = getSumOfScoreNodes().iterator(); iter.hasNext(); ) { - String nodeIdent = iter.next(); - sb.append("getScore(\""); - sb.append(nodeIdent); - sb.append("\")"); - if (iter.hasNext()) sb.append(" + "); + switch (scoreType) { + case SCORE_TYPE_SUM: return getSumScoreExpression(); + case SCORE_TYPE_AVG: return getAvgScoreExpression(); + default: // } - sb.append(")"); } + return null; + } - if (sb.length() == 0) { - return null; - } else { - return sb.toString(); + private String getSumScoreExpression() { + StringBuilder sb = new StringBuilder(); + for(Iterator<String> iter = getSumOfScoreNodes().iterator(); iter.hasNext(); ) { + String nodeIdent = iter.next(); + sb.append("getScore(\""); + sb.append(nodeIdent); + sb.append("\")"); + if (iter.hasNext()) sb.append(" + "); } + sb.append(")"); + return sb.toString(); + } + + private String getAvgScoreExpression() { + return new StringBuilder() + .append(GetAverageScoreFunction.NAME) + .append("(\"") + .append(getSumOfScoreNodes().stream().collect(joining("\",\""))) + .append("\")") + .toString(); } /** * Calculate the passed expression based on the easy mode configuration. This must not be used - * during calcualtion of a passed but after changeing an expression in the editor to set the - * new passed expression - * @return String + * during calculation of a passed but after changing an expression in the editor to set the + * new passed expression. + * + * @return */ public String getPassedExpressionFromEasyModeConfiguration() { if (getPassedType() == null || getPassedType().equals(PASSED_TYPE_NONE)) return null; @@ -146,11 +170,7 @@ public class ScoreCalculator implements Serializable { sb.append(getPassedCutValue()); } - if (sb.length() == 0) { - return null; - } else { - return sb.toString(); - } + return sb.length() > 0? sb.toString(): null; } /** @@ -160,69 +180,64 @@ public class ScoreCalculator implements Serializable { public boolean isExpertMode() { return expertMode; } + /** * @param expertMode true when in expert mode, false when in easy mode */ public void setExpertMode(boolean expertMode) { this.expertMode = expertMode; } + + public String getScoreType() { + return scoreType; + } + + public void setScoreType(String scoreType) { + this.scoreType = scoreType; + } + /** * @return List of nodeIdents as Strings */ public List<String> getSumOfScoreNodes() { return sumOfScoreNodes; } - /** - * @param sumOfScoreNodes - */ + public void setSumOfScoreNodes(List<String> sumOfScoreNodes) { this.sumOfScoreNodes = sumOfScoreNodes; } - /** - * @param passedExpression - */ + public void setPassedExpression(String passedExpression) { this.passedExpression = passedExpression; } - /** - * @param scoreExpression - */ + public void setScoreExpression(String scoreExpression) { this.scoreExpression = scoreExpression; } - /** - * @return int - */ + public int getPassedCutValue() { return passedCutValue; } - /** - * @param passedCutValue - */ + public void setPassedCutValue(int passedCutValue) { this.passedCutValue = passedCutValue; } + /** * @return List of nodeIdents as Strings */ public List<String> getPassedNodes() { return passedNodes; } - /** - * @param passedNodes - */ + public void setPassedNodes(List<String> passedNodes) { this.passedNodes = passedNodes; } - /** - * @return String - */ + public String getPassedType() { return passedType; } - /** - * @param passedType - */ + public void setPassedType(String passedType) { this.passedType = passedType; } @@ -240,10 +255,11 @@ public class ScoreCalculator implements Serializable { * */ public void clearEasyMode() { + scoreType = SCORE_TYPE_NONE; + sumOfScoreNodes = null; passedCutValue = 0; passedNodes = null; passedType = PASSED_TYPE_NONE; - sumOfScoreNodes = null; } } diff --git a/src/main/java/org/olat/course/run/scoring/ScoreEvaluation.java b/src/main/java/org/olat/course/run/scoring/ScoreEvaluation.java index c34c5412e5ac5135763a9d4cebe849ae5b67342a..f924ebff21d7bda6a6c36908b696be927b338fe6 100644 --- a/src/main/java/org/olat/course/run/scoring/ScoreEvaluation.java +++ b/src/main/java/org/olat/course/run/scoring/ScoreEvaluation.java @@ -132,9 +132,6 @@ public class ScoreEvaluation { return runStatus; } - /** (non-Javadoc) - * @see java.lang.Object#toString() - */ @Override public String toString() { return "score:" + score + ", passed:" + passed + ", fullyAssessed " + fullyAssessed + ", S" + hashCode();