From 92ba833a72b08a78525dfe0c86315bd330984eea Mon Sep 17 00:00:00 2001 From: srosse <stephane.rosse@frentix.com> Date: Fri, 13 Dec 2019 14:58:28 +0100 Subject: [PATCH] OO-4413: option in text entry to prevent twice the same input --- .../model/xml/AssessmentItemFactory.java | 10 +- .../FIBAssessmentItemBuilder.java | 398 +++++++++++++++--- .../AssessmentItemEditorController.java | 8 +- .../editor/_i18n/LocalStrings_de.properties | 2 + .../editor/_i18n/LocalStrings_en.properties | 2 + .../interactions/FIBEditorController.java | 26 +- .../interactions/FIBScoreController.java | 45 +- .../model/xml/AssessmentItemBuilderTest.java | 6 +- .../xml/FIBAssessmentItemBuilderTest.java | 320 ++++++++++++++ .../java/org/olat/test/AllTestsJunit4.java | 1 + 10 files changed, 712 insertions(+), 106 deletions(-) create mode 100644 src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java index 3a9fd43fed6..516efbcf4f4 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java @@ -331,7 +331,7 @@ public class AssessmentItemFactory { responseDeclaration.setIdentifier(declarationId); if(cardinality != null && (cardinality == Cardinality.SINGLE || cardinality == Cardinality.MULTIPLE)) { responseDeclaration.setCardinality(cardinality); - } else if(correctResponseIds == null || correctResponseIds.size() == 0 || correctResponseIds.size() > 1) { + } else if(correctResponseIds == null || correctResponseIds.isEmpty() || correctResponseIds.size() > 1) { responseDeclaration.setCardinality(Cardinality.MULTIPLE); } else { responseDeclaration.setCardinality(Cardinality.SINGLE); @@ -399,7 +399,7 @@ public class AssessmentItemFactory { } //map alternatives - if(alternatives != null && alternatives.size() > 0) { + if(alternatives != null && !alternatives.isEmpty()) { for(TextEntryAlternative alternative:alternatives) { if(StringHelper.containsNonWhitespace(alternative.getAlternative())) { MapEntry mapEntry = new MapEntry(mapping); @@ -1172,7 +1172,7 @@ public class AssessmentItemFactory { if(responseRule instanceof ResponseCondition) { ResponseCondition responseCondition = (ResponseCondition)responseRule; if(responseCondition.getResponseIf() == null || responseCondition.getResponseElse() != null - || (responseCondition.getResponseElseIfs() != null && responseCondition.getResponseElseIfs().size() > 0)) { + || (responseCondition.getResponseElseIfs() != null && !responseCondition.getResponseElseIfs().isEmpty())) { continue; } @@ -1739,7 +1739,7 @@ public class AssessmentItemFactory { public static String coordsString(List<Integer> coords) { StringBuilder sb = new StringBuilder(); - if(coords != null && coords.size() > 0) { + if(coords != null && !coords.isEmpty()) { for(Integer coord:coords) { if(sb.length() > 0) sb.append(","); sb.append(coord.intValue()); @@ -1753,7 +1753,7 @@ public class AssessmentItemFactory { if(StringHelper.containsNonWhitespace(coords)) { for(StringTokenizer tokenizer = new StringTokenizer(coords, ","); tokenizer.hasMoreElements(); ) { String coord = tokenizer.nextToken(); - list.add(new Integer(Math.round(Float.parseFloat(coord)))); + list.add(Integer.valueOf(Math.round(Float.parseFloat(coord)))); } } return list; diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java index 9292589235e..0c657b007db 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java @@ -36,8 +36,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.DoubleAdder; -import org.olat.core.gui.render.StringOutput; import org.apache.logging.log4j.Logger; +import org.olat.core.gui.render.StringOutput; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.ims.qti21.QTI21Constants; @@ -50,6 +50,7 @@ import uk.ac.ed.ph.jqtiplus.exception.QtiAttributeException; import uk.ac.ed.ph.jqtiplus.node.content.ItemBody; import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; import uk.ac.ed.ph.jqtiplus.node.expression.Expression; +import uk.ac.ed.ph.jqtiplus.node.expression.ExpressionParent; import uk.ac.ed.ph.jqtiplus.node.expression.general.BaseValue; import uk.ac.ed.ph.jqtiplus.node.expression.general.Correct; import uk.ac.ed.ph.jqtiplus.node.expression.general.MapResponse; @@ -57,6 +58,9 @@ import uk.ac.ed.ph.jqtiplus.node.expression.general.Variable; import uk.ac.ed.ph.jqtiplus.node.expression.operator.And; import uk.ac.ed.ph.jqtiplus.node.expression.operator.Equal; import uk.ac.ed.ph.jqtiplus.node.expression.operator.Match; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.Not; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.Or; +import uk.ac.ed.ph.jqtiplus.node.expression.operator.StringMatch; import uk.ac.ed.ph.jqtiplus.node.expression.operator.Sum; import uk.ac.ed.ph.jqtiplus.node.expression.operator.ToleranceMode; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; @@ -96,6 +100,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { private String question; private ScoreEvaluation scoreEvaluation; + private boolean allowDuplicatedAnswers; private Map<String, AbstractEntry> responseIdentifierToTextEntry; private QTI21QuestionType questionType = QTI21QuestionType.fib; @@ -140,6 +145,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { extractQuestions(); extractEntriesSettingsFromResponseDeclaration(); extractQuestionType(); + extractAllowDuplicatesAnswers(); } /** @@ -234,7 +240,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { Double solution = null; CorrectResponse correctResponse = responseDeclaration.getCorrectResponse(); - if(correctResponse != null && correctResponse.getFieldValues().size() > 0) { + if(correctResponse != null && !correctResponse.getFieldValues().isEmpty()) { List<FieldValue> fValues = correctResponse.getFieldValues(); SingleValue sValue = fValues.get(0).getSingleValue(); if(sValue instanceof FloatValue) { @@ -251,7 +257,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { if(responseRule instanceof ResponseCondition) { ResponseCondition condition = (ResponseCondition)responseRule; ResponseIf responseIf = condition.getResponseIf(); - if(responseIf != null && responseIf.getExpressions().size() > 0) { + if(responseIf != null && !responseIf.getExpressions().isEmpty()) { //first is an and/equal/ Expression potentialEqualOrAnd = responseIf.getExpressions().get(0); if(potentialEqualOrAnd instanceof And) { @@ -337,7 +343,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { String solution = null; CorrectResponse correctResponse = responseDeclaration.getCorrectResponse(); - if(correctResponse != null && correctResponse.getFieldValues().size() > 0) { + if(correctResponse != null && !correctResponse.getFieldValues().isEmpty()) { List<FieldValue> fValues = correctResponse.getFieldValues(); SingleValue sValue = fValues.get(0).getSingleValue(); if(sValue instanceof StringValue) { @@ -393,6 +399,67 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { } } + public void extractAllowDuplicatesAnswers() { + allowDuplicatedAnswers = true; + + List<ResponseRule> responseRules = assessmentItem.getResponseProcessing().getResponseRules(); + for(ResponseRule responseRule:responseRules) { + if(responseRule instanceof ResponseCondition) { + ResponseCondition responseCondition = (ResponseCondition)responseRule; + ResponseIf responseIf = responseCondition.getResponseIf(); + if(responseIf != null && !responseIf.getExpressions().isEmpty()) { + if(responseIf.getExpressions().size() == 1 && responseIf.getExpressions().get(0) instanceof Not) { + // check if the not is our check for duplication in case of score per answers + if(isNotForDuplicatesAnswers((Not)responseIf.getExpressions().get(0))) { + allowDuplicatedAnswers = false; + break; + } + } else if(responseIf.getExpressions().get(0) instanceof And) { + // check if the and contains the not for duplication if score is for all answers + And and = (And)responseIf.getExpressions().get(0); + + int numOfNot = 0; + int numOfMatch = 0; + for(Expression expression:and.getExpressions()) { + if(expression instanceof Match || expression instanceof Equal) { + numOfMatch++; + } else if(expression instanceof Not && isNotForDuplicatesAnswers((Not)expression)) { + numOfNot++; + } + } + + if(numOfMatch > 0 && numOfNot > 0) { + allowDuplicatedAnswers = false; + break; + } + } + } + } + } + } + + private boolean isNotForDuplicatesAnswers(Not not) { + if(not.getChildren().size() == 1 && not.getChildren().get(0) instanceof Or) { + Or or = (Or)not.getChildren().get(0); + for(Expression expression:or.getChildren()) { + if(expression instanceof StringMatch || expression instanceof Equal) { + List<Expression> variables = expression.getExpressions(); + if(variables.size() == 2 + && variables.get(0) instanceof Variable + && variables.get(1) instanceof Variable) { + Variable var1 = (Variable)variables.get(0); + Variable var2 = (Variable)variables.get(1); + String responseIdentifier1 = var1.getIdentifier().toString(); + String responseIdentifier2 = var2.getIdentifier().toString(); + return responseIdentifierToTextEntry.containsKey(responseIdentifier1) + && responseIdentifierToTextEntry.containsKey(responseIdentifier2); + } + } + } + } + return false; + } + public String escapeForDataQtiSolution(String solution) { return StringHelper.escapeHtml(solution).replace("/", "\u2215"); } @@ -416,6 +483,14 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { this.question = html; } + public boolean isAllowDuplicatedAnswers() { + return allowDuplicatedAnswers; + } + + public void setAllowDuplicatedAnswers(boolean allowDuplicatedAnswers) { + this.allowDuplicatedAnswers = allowDuplicatedAnswers; + } + public ScoreEvaluation getScoreEvaluationMode() { return scoreEvaluation; } @@ -449,7 +524,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { } } - if(entries.size() > 0) { + if(!entries.isEmpty()) { orderedEntries.addAll(entries);//security } return orderedEntries; @@ -622,17 +697,20 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { responseRules.add(0, rule); {// match all - ResponseIf responseElseIf = new ResponseIf(rule); - rule.setResponseIf(responseElseIf); - //rule.getResponseElseIfs().add(responseElseIf); + ResponseIf responseIf = new ResponseIf(rule); + rule.setResponseIf(responseIf); - And and = new And(responseElseIf); - responseElseIf.setExpression(and); + And and = new And(responseIf); + responseIf.setExpression(and); + + int numOfTextEntry = 0; for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) { AbstractEntry abstractEntry = textEntryEntry.getValue(); if(abstractEntry instanceof TextEntry) { + numOfTextEntry++; + Match match = new Match(and); and.getExpressions().add(match); @@ -673,10 +751,55 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { } } + if(!allowDuplicatedAnswers && numOfTextEntry > 1) { + /* + <not> + <or> + <stringMatch caseSensitive="false" substring="false"> + <variable identifier="RESPONSE_2"/> + <variable identifier="RESPONSE_1"/> + </stringMatch> + </or> + </not> + <not> + <or> + <stringMatch caseSensitive="false" substring="false"> + <variable identifier="RESPONSE_3"/> + <variable identifier="RESPONSE_2"/> + </stringMatch> + <stringMatch caseSensitive="false" substring="false"> + <variable identifier="RESPONSE_3"/> + <variable identifier="RESPONSE_1"/> + </stringMatch> + </or> + </not> + */ + + List<String> responseIdentifiers = new ArrayList<>(responseIdentifierToTextEntry.keySet()); + int numOfResponseIdentifiers = responseIdentifiers.size(); + for(int i=1; i<numOfResponseIdentifiers; i++) { + Not not = new Not(and); + Or or = new Or(not); + not.getExpressions().add(or); + + for(int j=i; j-->0; ) { + Expression match = match(responseIdentifiers.get(i), responseIdentifiers.get(j), not); + if(match != null) { + or.getExpressions().add(match); + } + } + + // in case of a mix numerical and text entries + if(!or.getExpressions().isEmpty()) { + and.getExpressions().add(not); + } + } + } + {// outcome max score -> score - SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseElseIf); + SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseIf); scoreOutcomeValue.setIdentifier(QTI21Constants.SCORE_IDENTIFIER); - responseElseIf.getResponseRules().add(scoreOutcomeValue); + responseIf.getResponseRules().add(scoreOutcomeValue); Sum sum = new Sum(scoreOutcomeValue); scoreOutcomeValue.getExpressions().add(sum); @@ -691,9 +814,9 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { } {//outcome feedback - SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseElseIf); + SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseIf); correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); - responseElseIf.getResponseRules().add(correctOutcomeValue); + responseIf.getResponseRules().add(correctOutcomeValue); BaseValue correctValue = new BaseValue(correctOutcomeValue); correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER); @@ -719,6 +842,82 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { } } + private Expression match(String responseIdentifier1, String responseIdentifier2, ExpressionParent parent) { + AbstractEntry entry1 = responseIdentifierToTextEntry.get(responseIdentifier1); + AbstractEntry entry2 = responseIdentifierToTextEntry.get(responseIdentifier2); + if(entry1 instanceof TextEntry && entry2 instanceof TextEntry + && shareSomeAlternatives((TextEntry)entry1, (TextEntry)entry2)) { + return stringMatch(responseIdentifier1, responseIdentifier2, parent); + } + return null; + } + + public boolean entriesSharesAlternatives() { + List<String> responseIdentifiers = new ArrayList<>(responseIdentifierToTextEntry.keySet()); + int numOfResponseIdentifiers = responseIdentifiers.size(); + for(int i=1; i<numOfResponseIdentifiers; i++) { + for(int j=i; j-->0; ) { + String responseIdentifier1 = responseIdentifiers.get(i); + String responseIdentifier2 = responseIdentifiers.get(j); + AbstractEntry entry1 = responseIdentifierToTextEntry.get(responseIdentifier1); + AbstractEntry entry2 = responseIdentifierToTextEntry.get(responseIdentifier2); + if(entry1 instanceof TextEntry && entry2 instanceof TextEntry + && shareSomeAlternatives((TextEntry)entry1, (TextEntry)entry2)) { + return true; + } + } + } + return false; + } + + public static boolean shareSomeAlternatives(TextEntry entry1, TextEntry entry2) { + List<String> alternatives1 = alternativesToString(entry1); + List<String> alternatives2 = alternativesToString(entry2); + for(String alt1:alternatives1) { + for(String alt2:alternatives2) { + if(alt1.compareToIgnoreCase(alt2) == 0) { + return true; + } + } + } + return false; + } + + private static List<String> alternativesToString(TextEntry entry) { + List<String> alternatives = new ArrayList<>(); + if(entry.getSolution() != null) { + alternatives.add(entry.getSolution()); + } + if(entry.getAlternatives() != null) { + for(TextEntryAlternative alternative:entry.getAlternatives()) { + alternatives.add(alternative.getAlternative()); + } + } + return alternatives; + } + + /* + <stringMatch caseSensitive="false" substring="false"> + <variable identifier="RESPONSE_3"/> + <variable identifier="RESPONSE_2"/> + </stringMatch> + */ + private StringMatch stringMatch(String responseIdentifier1, String responseIdentifier2, ExpressionParent parent) { + StringMatch stringMatch = new StringMatch(parent); + stringMatch.setCaseSensitive(Boolean.FALSE); + stringMatch.setSubString(Boolean.FALSE); + + Variable variable1 = new Variable(stringMatch); + variable1.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier1)); + Variable variable2 = new Variable(stringMatch); + variable2.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier2)); + + stringMatch.getExpressions().add(variable1); + stringMatch.getExpressions().add(variable2); + + return stringMatch; + } + @Override protected void buildModalFeedbacksAndHints(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) { if(correctFeedback != null || incorrectFeedback != null) { @@ -753,63 +952,13 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { */ int count = 0; - - for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) { - AbstractEntry entry = textEntryEntry.getValue(); + List<String> responseIdentifiers = new ArrayList<>(responseIdentifierToTextEntry.keySet()); + for(count = 0; count <responseIdentifiers.size(); count++) { + String responseStringIdentifier = responseIdentifiers.get(count); + AbstractEntry entry = responseIdentifierToTextEntry.get(responseStringIdentifier); String scoreIdentifier = "SCORE_" + entry.getResponseIdentifier().toString(); - - if(entry instanceof TextEntry) {//outcome mapResonse - SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(assessmentItem.getResponseProcessing()); - responseRules.add(count++, mapOutcomeValue); - mapOutcomeValue.setIdentifier(Identifier.parseString(scoreIdentifier)); - - MapResponse mapResponse = new MapResponse(mapOutcomeValue); - mapResponse.setIdentifier(entry.getResponseIdentifier()); - mapOutcomeValue.setExpression(mapResponse); - - } else if(entry instanceof NumericalEntry) { - NumericalEntry numericalEntry = (NumericalEntry)entry; - - ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing()); - responseRules.add(count++, rule); - - ResponseIf responseIf = new ResponseIf(rule); - rule.setResponseIf(responseIf); - - Equal equal = new Equal(responseIf); - equal.setToleranceMode(numericalEntry.getToleranceMode()); - if(numericalEntry.getLowerTolerance() != null && numericalEntry.getUpperTolerance() != null) { - List<FloatOrVariableRef> tolerances = new ArrayList<>(); - tolerances.add(new FloatOrVariableRef(numericalEntry.getLowerTolerance().doubleValue())); - tolerances.add(new FloatOrVariableRef(numericalEntry.getUpperTolerance().doubleValue())); - equal.setTolerances(tolerances); - } - equal.setIncludeLowerBound(Boolean.TRUE); - equal.setIncludeUpperBound(Boolean.TRUE); - responseIf.getExpressions().add(equal); - - ComplexReferenceIdentifier responseIdentifier = ComplexReferenceIdentifier - .assumedLegal(numericalEntry.getResponseIdentifier().toString()); - - Correct correct = new Correct(equal); - correct.setIdentifier(responseIdentifier); - equal.getExpressions().add(correct); - - Variable variable = new Variable(equal); - variable.setIdentifier(responseIdentifier); - equal.getExpressions().add(variable); - - SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(responseIf); - responseIf.getResponseRules().add(mapOutcomeValue); - mapOutcomeValue.setIdentifier(Identifier.parseString(scoreIdentifier)); - - BaseValue correctValue = new BaseValue(mapOutcomeValue); - correctValue.setBaseTypeAttrValue(BaseType.FLOAT); - correctValue.setSingleValue(new FloatValue(entry.getScore())); - mapOutcomeValue.setExpression(correctValue); - } + buildScoreRulePerAnswer(count, entry, Identifier.parseString(scoreIdentifier), responseIdentifiers, responseRules); } - /* <setOutcomeValue identifier="SCORE"> @@ -871,7 +1020,116 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder { } } - public static abstract class AbstractEntry { + private void buildScoreRulePerAnswer(int count, AbstractEntry entry, Identifier scoreIdentifier, + List<String> responseIdentifiers, List<ResponseRule> responseRules) { + if(entry instanceof TextEntry) { + buildScoreRulePerTextAnswer(count, (TextEntry)entry, scoreIdentifier, responseIdentifiers, responseRules); + } else if(entry instanceof NumericalEntry) { + buildScoreRulePerNumericalAnswer(count, (NumericalEntry)entry, scoreIdentifier, responseRules); + } + } + + /** + * Outcome map response. + * + * @param count Current position of the rule + * @param entry The text entry + * @param scoreIdentifier The identifier of the score + * @param responseRules The list of response rules + */ + private void buildScoreRulePerTextAnswer(int count, TextEntry entry, Identifier scoreIdentifier, List<String> responseIdentifiers, List<ResponseRule> responseRules) { + if(!allowDuplicatedAnswers && count > 0) { + ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing()); + + ResponseIf responseIf = new ResponseIf(rule); + rule.setResponseIf(responseIf); + + Not not = new Not(responseIf); + Or or = new Or(not); + not.getExpressions().add(or); + + for(int j=count; j-->0; ) { + Expression match = match(responseIdentifiers.get(count), responseIdentifiers.get(j), not); + if(match != null) { + or.getExpressions().add(match); + } + } + + // in case of a mix numerical and text entries + if(or.getExpressions().isEmpty()) { + SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(assessmentItem.getResponseProcessing()); + responseRules.add(count, mapOutcomeValue); + mapOutcomeValue.setIdentifier(scoreIdentifier); + + MapResponse mapResponse = new MapResponse(mapOutcomeValue); + mapResponse.setIdentifier(entry.getResponseIdentifier()); + mapOutcomeValue.setExpression(mapResponse); + } else { + responseIf.getExpressions().add(not); + responseRules.add(count, rule); + + SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(responseIf); + responseIf.getResponseRules().add(mapOutcomeValue); + + mapOutcomeValue.setIdentifier(scoreIdentifier); + + MapResponse mapResponse = new MapResponse(mapOutcomeValue); + mapResponse.setIdentifier(entry.getResponseIdentifier()); + mapOutcomeValue.setExpression(mapResponse); + } + } else { + SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(assessmentItem.getResponseProcessing()); + responseRules.add(count, mapOutcomeValue); + mapOutcomeValue.setIdentifier(scoreIdentifier); + + MapResponse mapResponse = new MapResponse(mapOutcomeValue); + mapResponse.setIdentifier(entry.getResponseIdentifier()); + mapOutcomeValue.setExpression(mapResponse); + } + } + + private void buildScoreRulePerNumericalAnswer(int count, NumericalEntry numericalEntry, Identifier scoreIdentifier, List<ResponseRule> responseRules) { + + ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing()); + responseRules.add(count, rule); + + ResponseIf responseIf = new ResponseIf(rule); + rule.setResponseIf(responseIf); + + Equal equal = new Equal(responseIf); + equal.setToleranceMode(numericalEntry.getToleranceMode()); + if(numericalEntry.getLowerTolerance() != null && numericalEntry.getUpperTolerance() != null) { + List<FloatOrVariableRef> tolerances = new ArrayList<>(); + tolerances.add(new FloatOrVariableRef(numericalEntry.getLowerTolerance().doubleValue())); + tolerances.add(new FloatOrVariableRef(numericalEntry.getUpperTolerance().doubleValue())); + equal.setTolerances(tolerances); + } + equal.setIncludeLowerBound(Boolean.TRUE); + equal.setIncludeUpperBound(Boolean.TRUE); + responseIf.getExpressions().add(equal); + + ComplexReferenceIdentifier responseIdentifier = ComplexReferenceIdentifier + .assumedLegal(numericalEntry.getResponseIdentifier().toString()); + + Correct correct = new Correct(equal); + correct.setIdentifier(responseIdentifier); + equal.getExpressions().add(correct); + + Variable variable = new Variable(equal); + variable.setIdentifier(responseIdentifier); + equal.getExpressions().add(variable); + + SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(responseIf); + responseIf.getResponseRules().add(mapOutcomeValue); + mapOutcomeValue.setIdentifier(scoreIdentifier); + + BaseValue correctValue = new BaseValue(mapOutcomeValue); + correctValue.setBaseTypeAttrValue(BaseType.FLOAT); + correctValue.setSingleValue(new FloatValue(numericalEntry.getScore())); + mapOutcomeValue.setExpression(correctValue); + } + + public abstract static class AbstractEntry { private Identifier responseIdentifier; private String placeholder; diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java index 085c5ae39ed..15b4bd6f72a 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java @@ -275,8 +275,8 @@ public class AssessmentItemEditorController extends BasicController implements A switch(type) { case sc: itemBuilder = initSingleChoiceEditors(ureq, item); break; case mc: itemBuilder = initMultipleChoiceEditors(ureq, item); break; - case fib: itemBuilder = initFIBEditors(ureq, type, item); break; - case numerical: itemBuilder = initFIBEditors(ureq, type, item); break; + case fib: itemBuilder = initFIBEditors(ureq, item); break; + case numerical: itemBuilder = initFIBEditors(ureq, item); break; case kprim: itemBuilder = initKPrimChoiceEditors(ureq, item); break; case match: itemBuilder = initMatchChoiceEditors(ureq, item); break; case matchdraganddrop: itemBuilder = initMatchDragAndDropEditors(ureq, item); break; @@ -408,9 +408,9 @@ public class AssessmentItemEditorController extends BasicController implements A return matchItemBuilder; } - private AssessmentItemBuilder initFIBEditors(UserRequest ureq, QTI21QuestionType preferedType, AssessmentItem item) { + private AssessmentItemBuilder initFIBEditors(UserRequest ureq, AssessmentItem item) { FIBAssessmentItemBuilder fibItemBuilder = new FIBAssessmentItemBuilder(item, qtiService.qtiSerializer()); - itemEditor = new FIBEditorController(ureq, getWindowControl(), preferedType, fibItemBuilder, + itemEditor = new FIBEditorController(ureq, getWindowControl(), fibItemBuilder, rootDirectory, rootContainer, itemFile, restrictedEdit, readOnly); listenTo(itemEditor); scoreEditor = new FIBScoreController(ureq, getWindowControl(), fibItemBuilder, itemRef, 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 71ddcb7325b..269232b56fe 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 @@ -117,6 +117,8 @@ form.imd.correctSolution.text=Korrekte L\u00F6sung form.imd.correctSolution.text.word=$\:form.imd.correctSolution.text (nur f\u00FCr Word Export) form.imd.correctSolution.title=Titel form.imd.descr=Frage +form.imd.duplicate.answers=Doppelte Eingaben erlauben +form.imd.duplicate.answers.hint=Limitiert zu Eingaben von Typ Text form.imd.empty.text=Feedback bei keiner Antwort form.imd.empty.title=Titel form.imd.feedback.text=Feedback 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 74227a3cb08..b56ba1a37c7 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 @@ -117,6 +117,8 @@ form.imd.correctSolution.text=Correct solution form.imd.correctSolution.text.word=$\:form.imd.correctSolution.text (only for Word export) form.imd.correctSolution.title=Title form.imd.descr=Question +form.imd.duplicate.answers=Allow twice the same input +form.imd.duplicate.answers.hint=Limited to input of type text form.imd.empty.text=Empty feedback form.imd.empty.title=Empty title form.imd.feedback.text=Feedback diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java index 19741c22651..384ef40ec5a 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java @@ -74,19 +74,17 @@ public class FIBEditorController extends FormBasicController { private final File itemFile; private final File rootDirectory; private final VFSContainer rootContainer; - private final QTI21QuestionType preferredType; - private final boolean restrictedEdit, readOnly; + private final boolean readOnly; + private final boolean restrictedEdit; private final FIBAssessmentItemBuilder itemBuilder; - public FIBEditorController(UserRequest ureq, WindowControl wControl, - QTI21QuestionType preferredType, FIBAssessmentItemBuilder itemBuilder, + public FIBEditorController(UserRequest ureq, WindowControl wControl, FIBAssessmentItemBuilder itemBuilder, File rootDirectory, VFSContainer rootContainer, File itemFile, boolean restrictedEdit, boolean readOnly) { super(ureq, wControl, LAYOUT_DEFAULT_2_10); setTranslator(Util.createPackageTranslator(AssessmentTestEditorController.class, getLocale())); this.itemFile = itemFile; this.itemBuilder = itemBuilder; - this.preferredType = preferredType; this.rootDirectory = rootDirectory; this.rootContainer = rootContainer; this.readOnly = readOnly; @@ -113,20 +111,8 @@ public class FIBEditorController extends FormBasicController { textEl.setElementCssClass("o_sel_assessment_item_fib_text"); RichTextConfiguration richTextConfig = textEl.getEditorConfiguration(); richTextConfig.setReadOnly(restrictedEdit || readOnly); - - boolean hasNumericals = itemBuilder.hasNumericalInputs(); - boolean hasTexts = itemBuilder.hasTextEntry(); - if(!hasNumericals && !hasTexts) { - if(preferredType == QTI21QuestionType.numerical) { - hasNumericals = true; - } else if(preferredType == QTI21QuestionType.fib) { - hasNumericals = false; - } else { - hasNumericals = true; - } - } richTextConfig.enableQTITools(true, true, false); - + // Submit Button FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); buttonsContainer.setElementCssClass("o_sel_fib_save"); @@ -210,7 +196,7 @@ public class FIBEditorController extends FormBasicController { @Override protected boolean validateFormLogic(UserRequest ureq) { - boolean allOk = true; + boolean allOk = super.validateFormLogic(ureq); String questionText = textEl.getRawValue(); if(!StringHelper.containsNonWhitespace(questionText)) { @@ -221,7 +207,7 @@ public class FIBEditorController extends FormBasicController { allOk &= false; } - return allOk & super.validateFormLogic(ureq); + return allOk; } @Override diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java index 4ba2e7b9157..670a9665ca2 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java @@ -56,7 +56,8 @@ import uk.ac.ed.ph.jqtiplus.types.Identifier; * */ public class FIBScoreController extends AssessmentItemRefEditorController implements SyncAssessmentItem { - + + private static final String[] yesnoKeys = new String[]{ "y", "n"}; private static final String[] modeKeys = new String[]{ ScoreEvaluation.allCorrectAnswers.name(), ScoreEvaluation.perAnswer.name() }; @@ -65,6 +66,7 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem private TextElement maxScoreEl; private FormLayoutContainer scoreCont; private SingleSelection assessmentModeEl; + private SingleSelection duplicateAllowedEl; private FIBAssessmentItemBuilder itemBuilder; private final List<FIBEntryWrapper> wrappers = new ArrayList<>(); @@ -93,6 +95,18 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem maxScoreEl.setElementCssClass("o_sel_assessment_item_max_score"); maxScoreEl.setEnabled(!restrictedEdit && !readOnly); + String[] yesnoValues = new String[]{ translate("yes"), translate("no") }; + duplicateAllowedEl = uifactory.addRadiosHorizontal("duplicate", "form.imd.duplicate.answers", formLayout, yesnoKeys, yesnoValues); + duplicateAllowedEl.setElementCssClass("o_sel_assessment_item_fib_duplicate"); + duplicateAllowedEl.setEnabled(!restrictedEdit && !readOnly); + duplicateAllowedEl.setVisible(hasSeveralTextEntryWithSharedAlternatives()); + duplicateAllowedEl.setHelpTextKey("form.imd.duplicate.answers.hint", null); + if(itemBuilder.isAllowDuplicatedAnswers()) { + duplicateAllowedEl.select(yesnoKeys[0], true); + } else { + duplicateAllowedEl.select(yesnoKeys[1], true); + } + String[] modeValues = new String[]{ translate("form.score.assessment.all.correct"), translate("form.score.assessment.per.answer") @@ -124,6 +138,19 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem formLayout.add(buttonsContainer); uifactory.addFormSubmitButton("submit", buttonsContainer); } + + private boolean hasSeveralTextEntryWithSharedAlternatives() { + int count = 0; + + List<AbstractEntry> entries = itemBuilder.getOrderedTextEntries(); + for(AbstractEntry entry:entries) { + if(entry instanceof TextEntry) { + count++; + } + } + + return count > 1 && itemBuilder.entriesSharesAlternatives(); + } @Override public void sync(UserRequest ureq, AssessmentItemBuilder assessmentItemBuilder) { @@ -167,6 +194,9 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem } wrappers.clear(); wrappers.addAll(reorderedWrappers); + + // duplicated only for text entry + duplicateAllowedEl.setVisible(hasSeveralTextEntryWithSharedAlternatives()); } } @@ -195,7 +225,7 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem @Override protected boolean validateFormLogic(UserRequest ureq) { - boolean allOk = true; + boolean allOk = super.validateFormLogic(ureq); allOk &= validateDouble(maxScoreEl); if(assessmentModeEl.isOneSelected() && assessmentModeEl.isSelected(1)) { @@ -204,7 +234,7 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem } } - return allOk & super.validateFormLogic(ureq); + return allOk; } @Override @@ -233,7 +263,14 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem String maxScoreValue = maxScoreEl.getValue(); Double maxScore = Double.parseDouble(maxScoreValue); itemBuilder.setMaxScore(maxScore); - itemBuilder.setMinScore(new Double(0d)); + itemBuilder.setMinScore(Double.valueOf(0.0d)); + + if(duplicateAllowedEl.isVisible()) { + boolean allowDuplicates = duplicateAllowedEl.isOneSelected() && duplicateAllowedEl.isSelected(0); + itemBuilder.setAllowDuplicatedAnswers(allowDuplicates); + } else { + itemBuilder.setAllowDuplicatedAnswers(true); + } if(assessmentModeEl.isOneSelected() && assessmentModeEl.isSelected(1)) { itemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); diff --git a/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java index a1d3b566a1b..b8003a4b033 100644 --- a/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java +++ b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java @@ -179,7 +179,7 @@ public class AssessmentItemBuilderTest { } @Test - public void buildAssessmentItem_gap() throws IOException, URISyntaxException { + public void buildAssessmentItem_textEntry() throws IOException, URISyntaxException { QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager()); FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Gap text", EntryType.text, qtiSerializer); if(build.booleanValue()) { @@ -257,7 +257,7 @@ public class AssessmentItemBuilderTest { * @return * @throws IOException */ - private ItemValidationResult serializeAndReload(AssessmentItem assessmentItem) throws IOException { + protected static ItemValidationResult serializeAndReload(AssessmentItem assessmentItem) throws IOException { QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager()); File tmpDir = new File(WebappHelper.getTmpDir(), "itembuilder" + UUID.randomUUID()); tmpDir.mkdirs(); @@ -287,7 +287,7 @@ public class AssessmentItemBuilderTest { return itemResult; } - private AssessmentItem loadAssessmentItem(URL itemUrl) throws URISyntaxException { + protected static AssessmentItem loadAssessmentItem(URL itemUrl) throws URISyntaxException { QtiXmlReader qtiXmlReader = new QtiXmlReader(new JqtiExtensionManager()); ResourceLocator fileResourceLocator = new PathResourceLocator(Paths.get(itemUrl.toURI())); AssessmentObjectXmlLoader assessmentObjectXmlLoader = new AssessmentObjectXmlLoader(qtiXmlReader, fileResourceLocator); diff --git a/src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java b/src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java new file mode 100644 index 00000000000..7c31df37d90 --- /dev/null +++ b/src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java @@ -0,0 +1,320 @@ +/** + * <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.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.logging.log4j.Logger; +import org.junit.Assert; +import org.junit.Test; +import org.olat.core.logging.Tracing; +import org.olat.core.util.FileUtils; +import org.olat.core.util.WebappHelper; +import org.olat.ims.qti21.QTI21Constants; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.EntryType; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntry; +import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntryAlternative; +import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder.ScoreEvaluation; + +import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager; +import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; +import uk.ac.ed.ph.jqtiplus.running.ItemSessionController; +import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer; +import uk.ac.ed.ph.jqtiplus.types.Identifier; +import uk.ac.ed.ph.jqtiplus.types.ResponseData; +import uk.ac.ed.ph.jqtiplus.types.StringResponseData; +import uk.ac.ed.ph.jqtiplus.validation.ItemValidationResult; +import uk.ac.ed.ph.jqtiplus.value.FloatValue; +import uk.ac.ed.ph.jqtiplus.value.Value; + +/** + * + * Initial date: 13 déc. 2019<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class FIBAssessmentItemBuilderTest { + + private static final Logger log = Tracing.createLoggerFor(FIBAssessmentItemBuilderTest.class); + + @Test + public void createTextEntry_text_duplicatesForbidden_scoreAll() throws IOException { + QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager()); + FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Only texts", EntryType.text, qtiSerializer); + + List<TextEntryAlternative> entryAlternatives = toAlternatives("Berset", "Sommaruga", "Cassis"); + + String responseIdentifier1 = itemBuilder.generateResponseIdentifier(); + TextEntry entry1 = itemBuilder.createTextEntry(responseIdentifier1); + entry1.setAlternatives(entryAlternatives); + entry1.setSolution("Sommaruga"); + entry1.setScore(1.0d); + + String responseIdentifier2 = itemBuilder.generateResponseIdentifier(); + TextEntry entry2 = itemBuilder.createTextEntry(responseIdentifier2); + entry2.setAlternatives(entryAlternatives); + entry2.setSolution("Berset"); + entry2.setScore(1.0d); + + itemBuilder.setQuestion("<p>New text <textEntryInteraction responseIdentifier=\"" + responseIdentifier1 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier2 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/></p>"); + itemBuilder.setMinScore(0.0d); + itemBuilder.setMaxScore(2.0d); + itemBuilder.setAllowDuplicatedAnswers(false); + itemBuilder.setScoreEvaluationMode(ScoreEvaluation.allCorrectAnswers); + itemBuilder.build(); + + ItemValidationResult itemResult = AssessmentItemBuilderTest.serializeAndReload(itemBuilder.getAssessmentItem()); + AssessmentItem reloadedItem = itemResult.getResolvedAssessmentItem().getItemLookup().extractIfSuccessful(); + List<Interaction> interactions = reloadedItem.getItemBody().findInteractions(); + Assert.assertEquals(2, interactions.size()); + + + File itemFile = new File(WebappHelper.getTmpDir(), "fibAssessmentItem" + UUID.randomUUID() + ".xml"); + try(FileOutputStream out = new FileOutputStream(itemFile)) { + qtiSerializer.serializeJqtiObject(itemBuilder.getAssessmentItem(), out); + } catch(Exception e) { + log.error("", e); + } + + {// correct answers + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Sommaruga")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Berset")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(2.0d), score); + } + + {// twice same answer + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Sommaruga")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Sommaruga")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(0.0d), score); + } + + {// wrong answer + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Werner")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Johnatan")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(0.0d), score); + } + + FileUtils.deleteDirsAndFiles(itemFile.toPath()); + } + + + @Test + public void createTextEntry_text_duplicatesForbidden_scorePerAnswers() throws IOException { + QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager()); + FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Only texts", EntryType.text, qtiSerializer); + + List<TextEntryAlternative> entryAlternatives = toAlternatives("Jupiter", "Saturne", "Uranus", "Neptune"); + + String responseIdentifier1 = itemBuilder.generateResponseIdentifier(); + TextEntry entry1 = itemBuilder.createTextEntry(responseIdentifier1); + entry1.setAlternatives(entryAlternatives); + entry1.setSolution("Jupiter"); + entry1.setScore(1.0d); + + String responseIdentifier2 = itemBuilder.generateResponseIdentifier(); + TextEntry entry2 = itemBuilder.createTextEntry(responseIdentifier2); + entry2.setAlternatives(entryAlternatives); + entry2.setSolution("Saturne"); + entry2.setScore(1.0d); + + String responseIdentifier3 = itemBuilder.generateResponseIdentifier(); + TextEntry entry3 = itemBuilder.createTextEntry(responseIdentifier3); + entry3.setAlternatives(entryAlternatives); + entry3.setSolution("Saturne"); + entry3.setScore(1.0d); + + itemBuilder.setQuestion("<p>Plan\u00E8te <textEntryInteraction responseIdentifier=\"" + responseIdentifier1 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier2 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier3 + "\"/></p>"); + itemBuilder.setMinScore(0.0d); + itemBuilder.setMaxScore(3.0d); + itemBuilder.setAllowDuplicatedAnswers(false); + itemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); + itemBuilder.build(); + + ItemValidationResult itemResult = AssessmentItemBuilderTest.serializeAndReload(itemBuilder.getAssessmentItem()); + AssessmentItem reloadedItem = itemResult.getResolvedAssessmentItem().getItemLookup().extractIfSuccessful(); + List<Interaction> interactions = reloadedItem.getItemBody().findInteractions(); + Assert.assertEquals(3, interactions.size()); + + File itemFile = new File(WebappHelper.getTmpDir(), "fibAssessmentItem" + UUID.randomUUID() + ".xml"); + try(FileOutputStream out = new FileOutputStream(itemFile)) { + qtiSerializer.serializeJqtiObject(itemBuilder.getAssessmentItem(), out); + } catch(Exception e) { + log.error("", e); + } + + {// correct answers + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Saturne")); + responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("uranus")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(3.0d), score); + } + + {// twice the same answer + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Saturne")); + responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Jupiter")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(2.0d), score); + } + + {// 3x the same answer + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Jupiter")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(1.0d), score); + } + + {// wrong answer + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Ceres")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Terre")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(0.0d), score); + } + + FileUtils.deleteDirsAndFiles(itemFile.toPath()); + } + + @Test + public void createTextEntry_text_duplicatesForbidden_nothingShared_scorePerAnswers() throws IOException { + QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager()); + FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Only texts", EntryType.text, qtiSerializer); + + List<TextEntryAlternative> bigAlternatives = toAlternatives("Jupiter", "Saturne", "Uranus", "Neptune"); + String responseIdentifier1 = itemBuilder.generateResponseIdentifier(); + TextEntry entry1 = itemBuilder.createTextEntry(responseIdentifier1); + entry1.setAlternatives(bigAlternatives); + entry1.setSolution("Jupiter"); + entry1.setScore(1.0d); + + List<TextEntryAlternative> smallAlternatives = toAlternatives("Terre", "Mercure", "Mars", "Venus"); + String responseIdentifier2 = itemBuilder.generateResponseIdentifier(); + TextEntry entry2 = itemBuilder.createTextEntry(responseIdentifier2); + entry2.setAlternatives(smallAlternatives); + entry2.setSolution("Terre"); + entry2.setScore(1.0d); + + List<TextEntryAlternative> verySmallAlternatives = toAlternatives("Pluton", "Ceres"); + String responseIdentifier3 = itemBuilder.generateResponseIdentifier(); + TextEntry entry3 = itemBuilder.createTextEntry(responseIdentifier3); + entry3.setAlternatives(verySmallAlternatives); + entry3.setSolution("Pluton"); + entry3.setScore(1.0d); + + itemBuilder.setQuestion("<p>Plan\u00E8te <textEntryInteraction responseIdentifier=\"" + responseIdentifier1 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier2 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier3 + "\"/></p>"); + itemBuilder.setMinScore(0.0d); + itemBuilder.setMaxScore(3.0d); + itemBuilder.setAllowDuplicatedAnswers(false); + itemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); + itemBuilder.build(); + + ItemValidationResult itemResult = AssessmentItemBuilderTest.serializeAndReload(itemBuilder.getAssessmentItem()); + AssessmentItem reloadedItem = itemResult.getResolvedAssessmentItem().getItemLookup().extractIfSuccessful(); + List<Interaction> interactions = reloadedItem.getItemBody().findInteractions(); + Assert.assertEquals(3, interactions.size()); + + File itemFile = new File(WebappHelper.getTmpDir(), "fibAssessmentItem" + UUID.randomUUID() + ".xml"); + try(FileOutputStream out = new FileOutputStream(itemFile)) { + qtiSerializer.serializeJqtiObject(itemBuilder.getAssessmentItem(), out); + } catch(Exception e) { + log.error("", e); + } + + {// correct answers + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Mars")); + responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Ceres")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(3.0d), score); + } + + {// twice the same answer, second is wrong + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Ceres")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(2.0d), score); + } + + {// 3x the same answer, two are wrong + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter")); + responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Jupiter")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(1.0d), score); + } + + {// wrong answer + Map<Identifier, ResponseData> responseMap = new HashMap<>(); + responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Ceres")); + responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter")); + ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap); + Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER); + Assert.assertEquals(new FloatValue(0.0d), score); + } + + FileUtils.deleteDirsAndFiles(itemFile.toPath()); + } + + private List<TextEntryAlternative> toAlternatives(String... strings) { + List<TextEntryAlternative> entryAlternatives = new ArrayList<>(); + for(String string:strings) { + TextEntryAlternative alternative = new TextEntryAlternative(); + alternative.setAlternative(string); + entryAlternatives.add(alternative); + } + return entryAlternatives; + } + + +} diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java index c87e9026217..e36fe09fe70 100644 --- a/src/test/java/org/olat/test/AllTestsJunit4.java +++ b/src/test/java/org/olat/test/AllTestsJunit4.java @@ -330,6 +330,7 @@ import org.junit.runners.Suite; org.olat.ims.qti21.model.xml.SingleChoiceAssessmentItemBuilderTest.class, org.olat.ims.qti21.model.xml.TestFeedbackBuilderTest.class, org.olat.ims.qti21.model.xml.HottextAssessmentItemBuilderTest.class, + org.olat.ims.qti21.model.xml.FIBAssessmentItemBuilderTest.class, org.olat.ims.qti21.model.xml.AssessmentHtmlBuilderTest.class, org.olat.ims.qti21.model.xml.AssessmentItemPackageTest.class, org.olat.ims.qti21.model.xml.ManifestPackageTest.class, -- GitLab