/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.ims.qti21.ui.editor.interactions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; import org.olat.core.gui.components.form.flexible.elements.SingleSelection; import org.olat.core.gui.components.form.flexible.elements.TextElement; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; import org.olat.core.util.Util; import org.olat.course.assessment.AssessmentHelper; import org.olat.ims.qti21.model.xml.AssessmentHtmlBuilder; import org.olat.ims.qti21.model.xml.AssessmentItemBuilder; import org.olat.ims.qti21.model.xml.ScoreBuilder; import org.olat.ims.qti21.model.xml.interactions.MatchAssessmentItemBuilder; import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder.ScoreEvaluation; import org.olat.ims.qti21.ui.editor.AssessmentTestEditorController; import org.olat.ims.qti21.ui.editor.SyncAssessmentItem; import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent; import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleAssociableChoice; import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleMatchSet; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentItemRef; import uk.ac.ed.ph.jqtiplus.types.Identifier; import uk.ac.ed.ph.jqtiplus.value.DirectedPairValue; /** * * Initial date: 22 nov. 2016<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class MatchScoreController extends AssessmentItemRefEditorController implements SyncAssessmentItem { private static final String[] modeKeys = new String[]{ ScoreEvaluation.allCorrectAnswers.name(), ScoreEvaluation.perAnswer.name() }; private TextElement minScoreEl; private TextElement maxScoreEl; private SingleSelection assessmentModeEl; private FormLayoutContainer scoreCont; private MatchAssessmentItemBuilder itemBuilder; private List<MatchWrapper> sourceWrappers = new ArrayList<>(); private List<MatchWrapper> targetWrappers = new ArrayList<>(); private Map<DirectedPairValue, MatchScoreWrapper> scoreWrappers = new HashMap<>(); public MatchScoreController(UserRequest ureq, WindowControl wControl, MatchAssessmentItemBuilder itemBuilder, AssessmentItemRef itemRef, boolean restrictedEdit) { super(ureq, wControl, itemRef, restrictedEdit); setTranslator(Util.createPackageTranslator(AssessmentTestEditorController.class, getLocale())); this.itemBuilder = itemBuilder; initForm(ureq); } @Override protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { super.initForm(formLayout, listener, ureq); setFormContextHelp("Test editor QTI 2.1 in detail#details_testeditor_score"); minScoreEl = uifactory.addTextElement("min.score", "min.score", 8, "0.0", formLayout); minScoreEl.setElementCssClass("o_sel_assessment_item_min_score"); minScoreEl.setEnabled(false); ScoreBuilder maxScore = itemBuilder.getMaxScoreBuilder(); String maxValue = maxScore == null ? "" : (maxScore.getScore() == null ? "" : maxScore.getScore().toString()); maxScoreEl = uifactory.addTextElement("max.score", "max.score", 8, maxValue, formLayout); maxScoreEl.setElementCssClass("o_sel_assessment_item_max_score"); maxScoreEl.setEnabled(!restrictedEdit); String[] modeValues = new String[]{ translate("form.score.assessment.all.correct"), translate("form.score.assessment.per.answer") }; assessmentModeEl = uifactory.addRadiosHorizontal("assessment.mode", "form.score.assessment.mode", formLayout, modeKeys, modeValues); assessmentModeEl.addActionListener(FormEvent.ONCHANGE); assessmentModeEl.setEnabled(!restrictedEdit); if(itemBuilder.getScoreEvaluationMode() == ScoreEvaluation.perAnswer) { assessmentModeEl.select(ScoreEvaluation.perAnswer.name(), true); } else { assessmentModeEl.select(ScoreEvaluation.allCorrectAnswers.name(), true); } String scorePage = velocity_root + "/match_score.html"; scoreCont = FormLayoutContainer.createCustomFormLayout("scores", getTranslator(), scorePage); formLayout.add(scoreCont); scoreCont.setLabel(null, null); scoreCont.setVisible(assessmentModeEl.isSelected(1)); for(SimpleAssociableChoice choice:itemBuilder.getSourceMatchSet().getSimpleAssociableChoices()) { sourceWrappers.add(createMatchWrapper(choice)); } scoreCont.contextPut("sourceChoices", sourceWrappers); for(SimpleAssociableChoice choice:itemBuilder.getTargetMatchSet().getSimpleAssociableChoices()) { targetWrappers.add(createMatchWrapper(choice)); } scoreCont.contextPut("targetChoices", targetWrappers); forgeScoreElements(); // Submit Button FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); buttonsContainer.setRootForm(mainForm); formLayout.add(buttonsContainer); uifactory.addFormSubmitButton("submit", buttonsContainer); } @Override public void sync(UserRequest ureq, AssessmentItemBuilder assessmentItemBuilder) { if(itemBuilder == assessmentItemBuilder) { sourceWrappers = sync(itemBuilder.getSourceMatchSet(), sourceWrappers); targetWrappers = sync(itemBuilder.getTargetMatchSet(), targetWrappers); scoreCont.contextPut("sourceChoices", sourceWrappers); scoreCont.contextPut("targetChoices", targetWrappers); forgeScoreElements(); } } private List<MatchWrapper> sync(SimpleMatchSet matchSet, List<MatchWrapper> wrappers) { Map<Identifier,MatchWrapper> currentMapping = wrappers.stream() .collect(Collectors.toMap(w -> w.getChoiceIdentifier(), w -> w)); List<MatchWrapper> newWrappers = new ArrayList<>(); List<SimpleAssociableChoice> choices = matchSet.getSimpleAssociableChoices(); for(SimpleAssociableChoice choice:choices) { if(currentMapping.containsKey(choice.getIdentifier())) { newWrappers.add(currentMapping.get(choice.getIdentifier())); } else { newWrappers.add(createMatchWrapper(choice)); } } return newWrappers; } private void forgeScoreElements() { for(MatchWrapper sourceWrapper:sourceWrappers) { for(MatchWrapper targetWrapper:targetWrappers) { forgeScoreElement(sourceWrapper, targetWrapper); } } } private void forgeScoreElement(MatchWrapper sourceWrapper, MatchWrapper targetWrapper) { Identifier sourceIdentifier = sourceWrapper.getChoiceIdentifier(); Identifier targetIdentifier = targetWrapper.getChoiceIdentifier(); DirectedPairValue dKey = new DirectedPairValue(sourceIdentifier, targetIdentifier); if(!scoreWrappers.containsKey(dKey)) { String key = sourceIdentifier.toString() + "-" + targetIdentifier.toString(); TextElement textEl = uifactory.addTextElement(key, null, 4, "", scoreCont); MatchScoreWrapper scoreWrapper = new MatchScoreWrapper(sourceIdentifier, targetIdentifier, textEl); textEl.setDomReplacementWrapperRequired(false); textEl.setDisplaySize(4); textEl.setUserObject(scoreWrapper); textEl.setEnabled(!restrictedEdit); Double score = itemBuilder.getScore(sourceIdentifier, targetIdentifier); if(score == null) { textEl.setValue("0.0"); } else { textEl.setValue(AssessmentHelper.getRoundedScore(score)); } scoreWrappers.put(dKey, scoreWrapper); scoreCont.contextPut(key, scoreWrapper); } } @Override protected boolean validateFormLogic(UserRequest ureq) { boolean allOk = true; allOk &= validateDouble(maxScoreEl); if(assessmentModeEl.isOneSelected() && assessmentModeEl.isSelected(1)) { /*for(HotspotChoiceWrapper wrapper:wrappers) { allOk &= validateDouble(wrapper.getPointsEl()); }*/ } return allOk & super.validateFormLogic(ureq); } @Override protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { if(assessmentModeEl.isOneSelected()) { scoreCont.setVisible(assessmentModeEl.isSelected(1)); } super.formInnerEvent(ureq, source, event); } @Override protected void formOK(UserRequest ureq) { super.formOK(ureq); String maxScoreValue = maxScoreEl.getValue(); Double maxScore = Double.parseDouble(maxScoreValue); itemBuilder.setMaxScore(maxScore); itemBuilder.setMinScore(new Double(0d)); if(assessmentModeEl.isOneSelected() && assessmentModeEl.isSelected(1)) { itemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer); itemBuilder.clearMapping(); for(Map.Entry<DirectedPairValue, MatchScoreWrapper> entry:scoreWrappers.entrySet()) { DirectedPairValue directedPair = entry.getKey(); MatchScoreWrapper scoreWrapper = entry.getValue(); String val = scoreWrapper.getScoreEl().getValue(); double score = Double.parseDouble(val); itemBuilder.addScore(directedPair, score); } } else { itemBuilder.setScoreEvaluationMode(ScoreEvaluation.allCorrectAnswers); itemBuilder.clearMapping(); } fireEvent(ureq, new AssessmentItemEvent(AssessmentItemEvent.ASSESSMENT_ITEM_CHANGED, itemBuilder.getAssessmentItem(), null)); } @Override protected void doDispose() { // } private MatchWrapper createMatchWrapper(SimpleAssociableChoice choice) { return new MatchWrapper(choice.getIdentifier(), choice); } public static class MatchWrapper { private final Identifier choiceIdentifier; private final SimpleAssociableChoice choice; private String summary; public MatchWrapper(Identifier choiceIdentifier, SimpleAssociableChoice choice) { this.choiceIdentifier = choiceIdentifier; this.choice = choice; if(choice != null) { summary = new AssessmentHtmlBuilder().flowStaticString(choice.getFlowStatics()); } else { summary = ""; } } public String getSummary() { return summary; } public Identifier getChoiceIdentifier() { return choiceIdentifier; } public SimpleAssociableChoice getChoice() { return choice; } } public class MatchScoreWrapper { private final Identifier sourceIdentifier; private final Identifier targetIdentifier; private final TextElement scoreEl; public MatchScoreWrapper(Identifier sourceIdentifier, Identifier targetIdentifier, TextElement scoreEl) { this.scoreEl = scoreEl; this.sourceIdentifier = sourceIdentifier; this.targetIdentifier = targetIdentifier; } public Identifier getSourceIdentifier() { return sourceIdentifier; } public Identifier getTargetIdentifier() { return targetIdentifier; } public TextElement getScoreEl() { return scoreEl; } public boolean isCorrect() { return itemBuilder.isCorrect(sourceIdentifier, targetIdentifier); } } }