From 900260f046e68baca79583ec4afbb97d51fc016a Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 8 Sep 2017 08:21:38 +0200
Subject: [PATCH] OO-2910: allow no answers for match and match with drag and
 drop

---
 .../model/xml/AssessmentItemFactory.java      |  86 ++++
 .../MatchAssessmentItemBuilder.java           | 295 +++++++-------
 .../interactions/MatchEditorController.java   |   3 +-
 .../java/org/olat/selenium/ImsQTI21Test.java  | 375 +++++++++++++++++-
 .../org/olat/selenium/page/qti/QTI21Page.java |   6 +
 5 files changed, 614 insertions(+), 151 deletions(-)

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 8824040add4..081db4707a3 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
@@ -48,6 +48,7 @@ import uk.ac.ed.ph.jqtiplus.node.content.xhtml.object.Object;
 import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.P;
 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;
 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.Gt;
@@ -79,6 +80,7 @@ import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.MapEntry;
 import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.Mapping;
 import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseCondition;
+import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseConditionChild;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElse;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElseIf;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseIf;
@@ -184,6 +186,90 @@ public class AssessmentItemFactory {
 		outcomeDeclarations.getOutcomeDeclarations().add(feedbackOutcomeDeclaration);
 	}
 	
+	/*
+	<setOutcomeValue identifier="FEEDBACKBASIC">
+		<baseValue baseType="identifier">
+			correct
+		</baseValue>
+	</setOutcomeValue>
+	*/
+	public static void appendSetOutcomeFeedbackCorrect(ResponseConditionChild responseCondition) {
+		SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseCondition);
+		correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
+		responseCondition.getResponseRules().add(correctOutcomeValue);
+		
+		BaseValue correctValue = new BaseValue(correctOutcomeValue);
+		correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
+		correctValue.setSingleValue(QTI21Constants.CORRECT_IDENTIFIER_VALUE);
+		correctOutcomeValue.setExpression(correctValue);
+	}
+	
+	/*
+	<setOutcomeValue identifier="FEEDBACKBASIC">
+		<baseValue baseType="identifier">incorrect</baseValue>
+	</setOutcomeValue>
+	*/
+	public static void appendSetOutcomeFeedbackIncorrect(ResponseConditionChild responseCondition) {
+		SetOutcomeValue incorrectOutcomeValue = new SetOutcomeValue(responseCondition);
+		incorrectOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
+		responseCondition.getResponseRules().add(incorrectOutcomeValue);
+		
+		BaseValue incorrectValue = new BaseValue(incorrectOutcomeValue);
+		incorrectValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
+		incorrectValue.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE);
+		incorrectOutcomeValue.setExpression(incorrectValue);
+	}
+	
+	/*
+    <setOutcomeValue identifier="SCORE">
+      <sum>
+        <variable identifier="SCORE"/>
+        <mapResponse identifier="RESPONSE_1"/>
+      </sum>
+    </setOutcomeValue>
+	*/
+	public static void appendSetOutcomeScoreMapResponse(ResponseConditionChild responseCondition, Identifier responseIdentifier) {
+		SetOutcomeValue scoreOutcome = new SetOutcomeValue(responseCondition);
+		scoreOutcome.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
+		responseCondition.getResponseRules().add(scoreOutcome);
+		
+		Sum sum = new Sum(scoreOutcome);
+		scoreOutcome.getExpressions().add(sum);
+		
+		Variable scoreVar = new Variable(sum);
+		scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER);
+		sum.getExpressions().add(scoreVar);
+		
+		MapResponse mapResponse = new MapResponse(sum);
+		mapResponse.setIdentifier(responseIdentifier);
+		sum.getExpressions().add(mapResponse);
+	}
+	
+	/*
+	<setOutcomeValue identifier="SCORE">
+	    <sum>
+	      <variable identifier="SCORE"/>
+	      <variable identifier="MAXSCORE"/>
+	    </sum>
+	  </setOutcomeValue>
+	*/
+	public static void appendSetOutcomeScoreMaxScore(ResponseConditionChild responseCondition) {
+		SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseCondition);
+		scoreOutcomeValue.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
+		responseCondition.getResponseRules().add(scoreOutcomeValue);
+		
+		Sum sum = new Sum(scoreOutcomeValue);
+		scoreOutcomeValue.getExpressions().add(sum);
+		
+		Variable scoreVar = new Variable(sum);
+		scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER);
+		sum.getExpressions().add(scoreVar);
+		
+		Variable maxScoreVar = new Variable(sum);
+		maxScoreVar.setIdentifier(QTI21Constants.MAXSCORE_CLX_IDENTIFIER);
+		sum.getExpressions().add(maxScoreVar);
+	}
+	
 	public static HotspotInteraction appendHotspotInteraction(ItemBody itemBody, Identifier responseDeclarationId, Identifier correctResponseId) {
 		HotspotInteraction hotspotInteraction = new HotspotInteraction(itemBody);
 		hotspotInteraction.setResponseIdentifier(responseDeclarationId);
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/MatchAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/MatchAssessmentItemBuilder.java
index 59a7d45439e..4f74399f851 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/MatchAssessmentItemBuilder.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/MatchAssessmentItemBuilder.java
@@ -23,6 +23,10 @@ import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendAssociati
 import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendDefaultItemBody;
 import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendDefaultOutcomeDeclarations;
 import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendMatchInteraction;
+import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendSetOutcomeFeedbackCorrect;
+import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendSetOutcomeFeedbackIncorrect;
+import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendSetOutcomeScoreMapResponse;
+import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendSetOutcomeScoreMaxScore;
 import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createMatchResponseDeclaration;
 import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createResponseProcessing;
 
@@ -44,14 +48,10 @@ import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuild
 import uk.ac.ed.ph.jqtiplus.group.NodeGroupList;
 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.general.BaseValue;
 import uk.ac.ed.ph.jqtiplus.node.expression.general.Correct;
-import uk.ac.ed.ph.jqtiplus.node.expression.general.MapResponse;
 import uk.ac.ed.ph.jqtiplus.node.expression.general.Variable;
 import uk.ac.ed.ph.jqtiplus.node.expression.operator.IsNull;
 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.Sum;
 import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
 import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.MatchInteraction;
@@ -62,17 +62,14 @@ import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.Mapping;
 import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseCondition;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElse;
-import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElseIf;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseIf;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseProcessing;
 import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseRule;
-import uk.ac.ed.ph.jqtiplus.node.item.response.processing.SetOutcomeValue;
 import uk.ac.ed.ph.jqtiplus.node.outcome.declaration.OutcomeDeclaration;
 import uk.ac.ed.ph.jqtiplus.node.shared.FieldValue;
 import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer;
 import uk.ac.ed.ph.jqtiplus.types.ComplexReferenceIdentifier;
 import uk.ac.ed.ph.jqtiplus.types.Identifier;
-import uk.ac.ed.ph.jqtiplus.value.BaseType;
 import uk.ac.ed.ph.jqtiplus.value.DirectedPairValue;
 import uk.ac.ed.ph.jqtiplus.value.SingleValue;
 
@@ -447,7 +444,11 @@ public class MatchAssessmentItemBuilder extends AssessmentItemBuilder {
 		ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing());
 		responseRules.add(0, rule);
 		if(scoreEvaluation == ScoreEvaluation.perAnswer) {
-			buildMainScoreRulePerAnswer(rule);
+			if(associations.isEmpty()) {
+				buildMainScoreRulePerAnswerNoAnswers(rule);
+			} else {
+				buildMainScoreRulePerAnswer(rule);
+			}
 		} else {
 			buildMainScoreRuleAllCorrectAnswers(rule);
 		}
@@ -455,102 +456,140 @@ public class MatchAssessmentItemBuilder extends AssessmentItemBuilder {
 	
 	@Override
 	protected void buildModalFeedbacksAndHints(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) {
-		if(correctFeedback != null || incorrectFeedback != null) {
-			if(scoreEvaluation == ScoreEvaluation.perAnswer) {
-				ResponseCondition responseCondition = AssessmentItemFactory.createModalFeedbackResponseConditionByScore(assessmentItem.getResponseProcessing());
-				responseRules.add(responseCondition);
-			}
-		}
-
 		super.buildModalFeedbacksAndHints(outcomeDeclarations, responseRules);
 	}
 	
-	private void buildMainScoreRulePerAnswer(ResponseCondition rule) {
+	/**
+	 * Special case where no answers are correct:<br>
+	 * <ul>
+	 * 	<li>If no answers chosen: maxScore + correct
+	 * 	<li>If answer chosen: map score of answers + incorrect
+	 * </ul>
+	 * 
+	 * @param rule
+	 */
+	private void buildMainScoreRulePerAnswerNoAnswers(ResponseCondition rule) {
 		/*
-		<responseCondition>
-			<responseIf>
-				<not>
-					<isNull>
-						<variable identifier="RESPONSE_4953445" />
-					</isNull>
-				</not>
-				<setOutcomeValue identifier="SCORE">
-					<sum>
-						<variable identifier="SCORE" /><mapResponse identifier="RESPONSE_4953445" />
-					</sum>
-				</setOutcomeValue>
-				<setOutcomeValue identifier="FEEDBACKBASIC">
-					<baseValue baseType="identifier">
-						incorrect
-					</baseValue>
-				</setOutcomeValue>
-			</responseIf>
-		</responseCondition>
-		<responseCondition>
-			<responseIf>
-				<and>
-					<not>
-						<match>
-							<variable identifier="FEEDBACKBASIC" />
-							<baseValue baseType="identifier">
-								empty
-							</baseValue>
-						</match>
-					</not>
-					<equal toleranceMode="exact">
-						<variable identifier="SCORE" /><variable identifier="MAXSCORE" />
-					</equal>
-				</and>
-				<setOutcomeValue identifier="FEEDBACKBASIC">
-					<baseValue baseType="identifier">
-						correct
-					</baseValue>
-				</setOutcomeValue>
-			</responseIf>
-		</responseCondition>
+	      <responseIf>
+	        <isNull>
+	          <variable identifier="RESPONSE_1"/>
+	        </isNull>
+	        <setOutcomeValue identifier="SCORE">
+	          <sum>
+	            <variable identifier="SCORE"/>
+	            <variable identifier="MAXSCORE"/>
+	          </sum>
+	        </setOutcomeValue>
+	        <setOutcomeValue identifier="FEEDBACKBASIC">
+	          <baseValue baseType="identifier">correct</baseValue>
+	        </setOutcomeValue>
+	      </responseIf>
+	      <responseElse>
+	        <setOutcomeValue identifier="SCORE">
+	          <sum>
+	            <variable identifier="SCORE"/>
+	            <mapResponse identifier="RESPONSE_1"/>
+	          </sum>
+	        </setOutcomeValue>
+	        <setOutcomeValue identifier="FEEDBACKBASIC">
+	          <baseValue baseType="identifier">incorrect</baseValue>
+	        </setOutcomeValue>
+	      </responseElse>
 		*/
 		
-		//if no response
 		ResponseIf responseIf = new ResponseIf(rule);
 		rule.setResponseIf(responseIf);
-		
-		Not not = new Not(responseIf);
-		responseIf.getExpressions().add(not);
 
-		IsNull isNull = new IsNull(not);
-		not.getExpressions().add(isNull);
+		IsNull isNull = new IsNull(responseIf);
+		responseIf.getExpressions().add(isNull);
 		
 		Variable variable = new Variable(isNull);
 		variable.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier.toString()));
 		isNull.getExpressions().add(variable);
 		
-		{// outcome score
-			SetOutcomeValue scoreOutcome = new SetOutcomeValue(responseIf);
-			scoreOutcome.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
-			responseIf.getResponseRules().add(scoreOutcome);
-			
-			Sum sum = new Sum(scoreOutcome);
-			scoreOutcome.getExpressions().add(sum);
-			
-			Variable scoreVar = new Variable(sum);
-			scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER);
-			sum.getExpressions().add(scoreVar);
+		//outcome sum score + max score
+		appendSetOutcomeScoreMaxScore(responseIf);
 			
-			MapResponse mapResponse = new MapResponse(sum);
-			mapResponse.setIdentifier(responseIdentifier);
-			sum.getExpressions().add(mapResponse);
-		}
+		//outcome correct feedback
+		appendSetOutcomeFeedbackCorrect(responseIf);
 		
-		{//outcome feedback
-			SetOutcomeValue incorrectOutcomeValue = new SetOutcomeValue(responseIf);
-			incorrectOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
-			responseIf.getResponseRules().add(incorrectOutcomeValue);
-			
-			BaseValue incorrectValue = new BaseValue(incorrectOutcomeValue);
-			incorrectValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
-			incorrectValue.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE);
-			incorrectOutcomeValue.setExpression(incorrectValue);
-		}
+		ResponseElse responseElse = new ResponseElse(rule);
+		rule.setResponseElse(responseElse);
+		// outcome score
+		appendSetOutcomeScoreMapResponse(responseElse, responseIdentifier);
+		//outcome incorrect feedback
+		appendSetOutcomeFeedbackIncorrect(responseElse);
+	}
+	
+	/**
+	 * Case with some correct answers and scoring per answer:
+	 * <ul>
+	 *  <li>All correct: calculate score + correct
+	 *  <li>Else: calculate score + incorrect
+	 * </ul>
+	 * 
+	 * @param rule
+	 */
+	private void buildMainScoreRulePerAnswer(ResponseCondition rule) {
+		/*
+		<responseCondition>
+	      <responseIf>
+	        <match>
+	          <variable identifier="RESPONSE_1"/>
+	          <correct identifier="RESPONSE_1"/>
+	        </match>
+	        <setOutcomeValue identifier="SCORE">
+	          <sum>
+	            <variable identifier="SCORE"/>
+	            <mapResponse identifier="RESPONSE_1"/>
+	          </sum>
+	        </setOutcomeValue>
+	        <setOutcomeValue identifier="FEEDBACKBASIC">
+	          <baseValue baseType="identifier">correct</baseValue>
+	        </setOutcomeValue>
+	      </responseIf>
+	      <responseElse>
+	        <setOutcomeValue identifier="SCORE">
+	          <sum>
+	            <variable identifier="SCORE"/>
+	            <mapResponse identifier="RESPONSE_1"/>
+	          </sum>
+	        </setOutcomeValue>
+	        <setOutcomeValue identifier="FEEDBACKBASIC">
+	          <baseValue baseType="identifier">incorrect</baseValue>
+	        </setOutcomeValue>
+	      </responseElse>
+		</responseCondition>
+		*/
+		
+		//if no response
+		ResponseIf responseIf = new ResponseIf(rule);
+		rule.setResponseIf(responseIf);
+		Match match = new Match(responseIf);
+		responseIf.getExpressions().add(match);
+		
+		Variable responseVar = new Variable(match);
+		ComplexReferenceIdentifier choiceResponseIdentifier
+			= ComplexReferenceIdentifier.parseString(responseIdentifier.toString());
+		responseVar.setIdentifier(choiceResponseIdentifier);
+		match.getExpressions().add(responseVar);
+		
+		Correct correct = new Correct(match);
+		correct.setIdentifier(choiceResponseIdentifier);
+		match.getExpressions().add(correct);
+		
+		// outcome score
+		appendSetOutcomeScoreMapResponse(responseIf, responseIdentifier);
+		//outcome correct feedback
+		appendSetOutcomeFeedbackCorrect(responseIf);
+		
+		ResponseElse responseElse = new ResponseElse(rule);
+		rule.setResponseElse(responseElse);
+		
+		// outcome score
+		appendSetOutcomeScoreMapResponse(responseElse, responseIdentifier);
+		// outcome  incorrect feedback
+		appendSetOutcomeFeedbackIncorrect(responseElse);
 	}
 
 	private void buildMainScoreRuleAllCorrectAnswers(ResponseCondition rule) {
@@ -599,82 +638,40 @@ public class MatchAssessmentItemBuilder extends AssessmentItemBuilder {
 		ResponseIf responseIf = new ResponseIf(rule);
 		rule.setResponseIf(responseIf);
 		
-		{//if no response
+		// match the correct answers (or null if there are no associations)
+		if(associations.isEmpty()) {
 			IsNull isNull = new IsNull(responseIf);
 			responseIf.getExpressions().add(isNull);
 			
-			Variable variable = new Variable(isNull);
-			variable.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier.toString()));
-			isNull.getExpressions().add(variable);
-			
-			SetOutcomeValue incorrectOutcomeValue = new SetOutcomeValue(responseIf);
-			incorrectOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
-			responseIf.getResponseRules().add(incorrectOutcomeValue);
-			
-			BaseValue incorrectValue = new BaseValue(incorrectOutcomeValue);
-			incorrectValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
-			incorrectValue.setSingleValue(QTI21Constants.EMPTY_IDENTIFIER_VALUE);
-			incorrectOutcomeValue.setExpression(incorrectValue);
-		}
-		
-		ResponseElseIf responseElseIf = new ResponseElseIf(rule);
-		rule.getResponseElseIfs().add(responseElseIf);
-		
-		{// match the correct answers
-			Match match = new Match(responseElseIf);
-			responseElseIf.getExpressions().add(match);
+			Variable responseVar = new Variable(isNull);
+			ComplexReferenceIdentifier choiceResponseIdentifier
+				= ComplexReferenceIdentifier.parseString(responseIdentifier.toString());
+			responseVar.setIdentifier(choiceResponseIdentifier);
+			isNull.getExpressions().add(responseVar);
+		} else {
+			Match match = new Match(responseIf);
+			responseIf.getExpressions().add(match);
 			
-			Variable scoreVar = new Variable(match);
+			Variable responseVar = new Variable(match);
 			ComplexReferenceIdentifier choiceResponseIdentifier
 				= ComplexReferenceIdentifier.parseString(responseIdentifier.toString());
-			scoreVar.setIdentifier(choiceResponseIdentifier);
-			match.getExpressions().add(scoreVar);
+			responseVar.setIdentifier(choiceResponseIdentifier);
+			match.getExpressions().add(responseVar);
 			
 			Correct correct = new Correct(match);
 			correct.setIdentifier(choiceResponseIdentifier);
 			match.getExpressions().add(correct);
 		}
 	
-		{//outcome score
-			SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseElseIf);
-			scoreOutcomeValue.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
-			responseElseIf.getResponseRules().add(scoreOutcomeValue);
+		//outcome score + max score
+		appendSetOutcomeScoreMaxScore(responseIf);
 			
-			Sum sum = new Sum(scoreOutcomeValue);
-			scoreOutcomeValue.getExpressions().add(sum);
-			
-			Variable scoreVar = new Variable(sum);
-			scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER);
-			sum.getExpressions().add(scoreVar);
-			
-			Variable maxScoreVar = new Variable(sum);
-			maxScoreVar.setIdentifier(QTI21Constants.MAXSCORE_CLX_IDENTIFIER);
-			sum.getExpressions().add(maxScoreVar);
-		}
-			
-		{//outcome feedback
-			SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseElseIf);
-			correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
-			responseElseIf.getResponseRules().add(correctOutcomeValue);
-			
-			BaseValue correctValue = new BaseValue(correctOutcomeValue);
-			correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
-			correctValue.setSingleValue(QTI21Constants.CORRECT_IDENTIFIER_VALUE);
-			correctOutcomeValue.setExpression(correctValue);
-		}
+		//outcome correct feedback
+		appendSetOutcomeFeedbackCorrect(responseIf);
 		
 		ResponseElse responseElse = new ResponseElse(rule);
 		rule.setResponseElse(responseElse);
-		
-		{// outcome feedback
-			SetOutcomeValue incorrectOutcomeValue = new SetOutcomeValue(responseElse);
-			incorrectOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
-			responseElse.getResponseRules().add(incorrectOutcomeValue);
-			
-			BaseValue incorrectValue = new BaseValue(incorrectOutcomeValue);
-			incorrectValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
-			incorrectValue.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE);
-			incorrectOutcomeValue.setExpression(incorrectValue);
-		}
+		// outcome incorrect feedback
+		appendSetOutcomeFeedbackIncorrect(responseElse);
 	}
 }
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MatchEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MatchEditorController.java
index 71221c85dc4..bae0eae92df 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MatchEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/MatchEditorController.java
@@ -243,7 +243,7 @@ public class MatchEditorController extends FormBasicController {
 		}
 		
 		commitTemporaryAssociations(ureq);
-		
+		/*
 		if(singleMultiEl.isOneSelected() && singleMultiEl.isSelected(0)) {
 			Map<String,String> sourseTargetMap = new HashMap<>();
 			String[] directedPairsIds = ureq.getHttpReq().getParameterValues("qtiworks_response_" + itemBuilder.getResponseIdentifier());
@@ -278,6 +278,7 @@ public class MatchEditorController extends FormBasicController {
 				}
 			}
 		}
+		*/
 		
 		if(layoutEl != null) {
 			layoutEl.clearError();
diff --git a/src/test/java/org/olat/selenium/ImsQTI21Test.java b/src/test/java/org/olat/selenium/ImsQTI21Test.java
index ce929373d6b..57cbea9b2b5 100644
--- a/src/test/java/org/olat/selenium/ImsQTI21Test.java
+++ b/src/test/java/org/olat/selenium/ImsQTI21Test.java
@@ -1611,7 +1611,193 @@ public class ImsQTI21Test {
 			.assertOnAssessmentTestScore(10);// 4 points from the first question, 6 from the second
 	}
 	
-
+	/**
+	 * An author make a test with 2 matches. A match with "multiple selection"
+	 * and score "all answers", a second with "single selection" and score
+	 * "per answers". They are distractors, the assessed user must let them blank.<br>
+	 * A first user make the test, but doesn't answer all questions
+	 * correctly, log out and a second user make the perfect test.
+	 * 
+	 * @param authorLoginPage
+	 * @param participantBrowser
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21EditorMatch_distractors(@InitialPage LoginPage authorLoginPage,
+			@Drone @User WebDriver participantBrowser)
+	throws IOException, URISyntaxException {
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		UserVO rei = new UserRestClient(deploymentUrl).createRandomUser("Rei");
+		UserVO melissa = new UserRestClient(deploymentUrl).createRandomUser("Melissa");
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		String qtiTestTitle = "Match QTI 2.1 " + UUID.randomUUID();
+		navBar
+			.openAuthoringEnvironment()
+			.createQTI21Test(qtiTestTitle)
+			.clickToolbarBack();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		QTI21EditorPage qtiEditor = qtiPage
+				.edit();
+		//start a blank test
+		qtiEditor
+			.selectNode("Single choice")
+			.deleteNode();
+		
+		//add a match, multiple selection
+		QTI21MatchEditorPage matchEditor = qtiEditor
+			.addMatch();
+		matchEditor
+			.setSource(0, "Eclipse")
+			.setSource(1, "nano")
+			.setTarget(0, "IDE")
+			.setTarget(1, "WordProcessor")
+			.addColumn()
+			.setTarget(2, "CAD")
+			.save();
+		// change max score
+		matchEditor
+			.selectScores()
+			.setMaxScore("4")
+			.save();
+		// set some feedbacks
+		matchEditor
+			.selectFeedbacks()
+			.setHint("Hint", "This is only an hint")
+			.setCorrectSolution("Correct solution", "This is the correct solution")
+			.setCorrectFeedback("Correct feedback", "This is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		// second match
+		matchEditor = qtiEditor
+			.addMatch()
+			.setSingleChoices()
+			.setSource(0, "Java")
+			.setSource(1, "C")
+			.addRow()
+			.setSource(2, "PHP")
+			.setTarget(0, "Lynx")
+			.setTarget(1, "Netscape")
+			.addColumn()
+			.setTarget(2, "Pixel")
+			.save();
+		// select score "per answer" and set the scores
+		matchEditor
+			.selectScores()
+			.selectAssessmentMode(ScoreEvaluation.perAnswer)
+			.setMaxScore("6")
+			.setScore(0, 0, "0.0")
+			.setScore(0, 1, "0.0")
+			.setScore(0, 2, "1.0")
+			.setScore(1, 0, "0.0")
+			.setScore(1, 1, "1.0")
+			.setScore(1, 2, "0.0")
+			.setScore(2, 0, "2.0")
+			.setScore(2, 1, "0.0")
+			.setScore(2, 2, "-0.5")
+			.save();
+		matchEditor
+			.selectFeedbacks()
+			.setHint("Hint", "The hint")
+			.setCorrectSolution("Correct solution", "This is the correct solution")
+			.setCorrectFeedback("Correct feedback", "This is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		qtiPage
+			.clickToolbarBack();
+		// access to all
+		qtiPage
+			.accessConfiguration()
+			.setUserAccess(UserAccess.guest)
+			.clickToolbarBack();
+		// show results
+		qtiPage
+			.options()
+			.showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions())
+			.save();
+		
+		//a user search the content package
+		LoginPage reiLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		reiLoginPage
+			.loginAs(rei.getLogin(), rei.getPassword())
+			.resume();
+		NavigationPage reiNavBar = new NavigationPage(participantBrowser);
+		reiNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page reiQtiPage = QTI21Page
+				.getQTI12Page(participantBrowser);
+		reiQtiPage
+			.assertOnAssessmentItem()
+			.answerMatch("Eclipse", "WordProcessor", true)
+			.answerMatch("nano", "CAD", true)
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertCorrectSolution("Correct solution")
+			.hint()
+			.assertFeedback("Hint")
+			.answerMatch("nano", "CAD", false)
+			.answerMatch("Eclipse", "WordProcessor", false)
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerMatch("Java", "Pixel", true)
+			.answerMatch("C", "Lynx", true)
+			.answerMatch("PHP", "Pixel", true)
+			.saveAnswer()
+			.assertCorrectSolution("Correct solution")
+			.assertFeedback("Incorrect")
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore("4.5");// 4 points from the first question, 0.5 from the second
+		
+		//a second user search the content package
+		LoginPage melLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		melLoginPage
+			.loginAs(melissa.getLogin(), melissa.getPassword())
+			.resume();
+		NavigationPage melNavBar = new NavigationPage(participantBrowser);
+		melNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page
+			.getQTI12Page(participantBrowser)
+			.assertOnAssessmentItem()
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerMatch("Java", "Pixel", true)
+			.answerMatch("C", "Pixel", true)
+			.answerMatch("PHP", "Lynx", true)
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertCorrectSolution("Correct solution")
+			.answerMatch("Java", "Pixel", false)
+			.answerMatch("C", "Pixel", false)
+			.answerMatch("PHP", "Lynx", false)
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(10);// 4 points from the first question, 6 from the second
+	}
+	
 	/**
 	 * An author make a test with 2 match of the drag and drop variety
 	 * with feedbacks.<br>
@@ -1801,6 +1987,193 @@ public class ImsQTI21Test {
 			.assertOnAssessmentTestScore(11);// 4 points from the first question, 7 from the second	
 	}
 	
+	/**
+	 * An author make a test with 2 match of the drag and drop variety
+	 * with feedbacks but as distractor. The assessed user need to let them
+	 * blank to have the max. score.<br>
+	 * A first user make the test, check the feedbacks but make an error
+	 * and score the maximum. A second user answers all the questions
+	 * correctly.
+	 * 
+	 * @param authorLoginPage
+	 * @param participantBrowser
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21EditorMatchDragAndDrop_distractors(@InitialPage LoginPage authorLoginPage,
+			@Drone @User WebDriver participantBrowser)
+	throws IOException, URISyntaxException {
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		UserVO asuka = new UserRestClient(deploymentUrl).createRandomUser("Asuka");
+		UserVO chara = new UserRestClient(deploymentUrl).createRandomUser("Chara");
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		String qtiTestTitle = "Match DnD QTI 2.1 " + UUID.randomUUID();
+		navBar
+			.openAuthoringEnvironment()
+			.createQTI21Test(qtiTestTitle)
+			.clickToolbarBack();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		QTI21EditorPage qtiEditor = qtiPage
+				.edit();
+		//start a blank test
+		qtiEditor
+			.selectNode("Single choice")
+			.deleteNode();
+		
+		//add a match, multiple selection
+		QTI21MatchEditorPage matchEditor = qtiEditor
+			.addMatchDragAndDrop();
+		matchEditor
+			.setSource(0, "Einstein")
+			.setSource(1, "Planck")
+			.addRow()
+			.setSource(2, "Euler")
+			.setTarget(0, "Chemistry")
+			.setTarget(1, "Philosophy")
+			.save();
+		// change max score
+		matchEditor
+			.selectScores()
+			.setMaxScore("4")
+			.save();
+		// set some feedbacks
+		matchEditor
+			.selectFeedbacks()
+			.setHint("Hint", "Euler come from Switzerland")
+			.setCorrectSolution("Correct solution", "The correct solution is simple")
+			.setCorrectFeedback("Correct feedback", "You are right")
+			.setIncorrectFeedback("Incorrect", "Your answer is not exactly correct")
+			.save();
+		
+		// second match
+		matchEditor = qtiEditor
+			.addMatchDragAndDrop()
+			.setSingleChoices()
+			.setSource(0, "Euler")
+			.setSource(1, "Broglie")
+			.addRow()
+			.setSource(2, "Konrad")
+			.setTarget(0, "Chemistry")
+			.setTarget(1, "Biology")
+			.addColumn()
+			.setTarget(2, "Astrology")
+			.save();
+		// select score "per answer" and set the scores
+		matchEditor
+			.selectScores()
+			.selectAssessmentMode(ScoreEvaluation.perAnswer)
+			.setMaxScore("8")
+			.setScore(0, 0, "1.0")
+			.setScore(0, 1, "0.0")
+			.setScore(0, 2, "0.0")
+			.setScore(1, 0, "0.0")
+			.setScore(1, 1, "0.0")
+			.setScore(1, 2, "-0.5")
+			.setScore(2, 0, "0.0")
+			.setScore(2, 1, "2.0")
+			.setScore(2, 2, "0.0")
+			.save();
+		matchEditor
+			.selectFeedbacks()
+			.setHint("Hint", "The hint")
+			.setCorrectSolution("Correct solution", "This is the correct solution")
+			.setCorrectFeedback("Correct feedback", "This is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		//close editor
+		qtiPage
+			.clickToolbarBack();
+		// access to all
+		qtiPage
+			.accessConfiguration()
+			.setUserAccess(UserAccess.guest)
+			.clickToolbarBack();
+		// show results
+		qtiPage
+			.options()
+			.showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions())
+			.save();
+		
+		//a user search the content package
+		LoginPage asukaLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		asukaLoginPage
+			.loginAs(asuka.getLogin(), asuka.getPassword())
+			.resume();
+		NavigationPage asukaNavBar = new NavigationPage(participantBrowser);
+		asukaNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page asukaQtiPage = QTI21Page
+				.getQTI12Page(participantBrowser);
+		asukaQtiPage
+			.assertOnAssessmentItem()
+			.answerMatchDropSourceToTarget("Einstein", "Chemistry")
+			.answerMatchDropSourceToTarget("Planck", "Philosophy")
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertCorrectSolution("Correct solution")
+			.hint()
+			.assertFeedback("Hint")
+			.answerMatchDetarget("Planck")
+			.answerMatchDetarget("Einstein")
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerMatchDropSourceToTarget("Broglie", "Astrology") // -0.5 points
+			.answerMatchDropSourceToTarget("Euler", "Chemistry")  // 1 points
+			.answerMatchDropSourceToTarget("Konrad", "Chemistry") // 0 points
+			.saveAnswer()
+			.assertCorrectSolution("Correct solution")
+			.assertFeedback("Incorrect")
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore("4.5");
+		
+		//a second user search the content package
+		LoginPage charaLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		charaLoginPage
+			.loginAs(chara.getLogin(), chara.getPassword())
+			.resume();
+		NavigationPage charaNavBar = new NavigationPage(participantBrowser);
+		charaNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page
+			.getQTI12Page(participantBrowser)
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerMatchDropSourceToTarget("Broglie", "Chemistry")   // 2 points
+			.answerMatchDropSourceToTarget("Euler", "Astrology") // 2 points
+			.answerMatchDropSourceToTarget("Konrad", "Astrology")   // 3 points
+			.saveAnswer()
+			.assertCorrectSolution("Correct solution")
+			.assertFeedback("Incorrect")
+			.answerMatchDetarget("Broglie")
+			.answerMatchDetarget("Euler")
+			.answerMatchDetarget("Konrad")
+			.saveAnswer()
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(12);// 4 points from the first question, 8 from the second	
+	}
+	
 	/**
 	 * An author make a test with 1 upload and feedbacks.<br>
 	 * A user make the test, test hint and upload the file.
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
index eaefc7615ac..7db9c628557 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
@@ -368,6 +368,12 @@ public class QTI21Page {
 		return this;
 	}
 	
+	public QTI21Page assertOnAssessmentTestScore(String score) {
+		By resultsBy = By.xpath("//div[contains(@class,'o_sel_results_details')]//tr[contains(@class,'o_sel_assessmenttest_scores')]/td/div/span[contains(@class,'o_sel_assessmenttest_score')][contains(text(),'" + score + "')]");
+		OOGraphene.waitElement(resultsBy, 5, browser);
+		return this;
+	}
+	
 	public QTI21Page assertOnAssessmentTestPassed() {
 		By notPassedBy = By.cssSelector("div.o_sel_results_details tr.o_qti_stateinfo.o_passed");
 		OOGraphene.waitElement(notPassedBy, 5, browser);
-- 
GitLab