/**
* <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.io.File;
import java.util.ArrayList;
import java.util.List;
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.FormLink;
import org.olat.core.gui.components.form.flexible.elements.RichTextElement;
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.FormBasicController;
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.components.link.Link;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.util.StringHelper;
import org.olat.core.util.Util;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.ims.qti21.QTI21Constants;
import org.olat.ims.qti21.model.IdentifierGenerator;
import org.olat.ims.qti21.model.QTI21QuestionType;
import org.olat.ims.qti21.model.xml.AssessmentItemFactory;
import org.olat.ims.qti21.model.xml.interactions.MultipleChoiceAssessmentItemBuilder;
import org.olat.ims.qti21.ui.editor.AssessmentTestEditorController;
import org.olat.ims.qti21.ui.editor.events.AssessmentItemEvent;
import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.P;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.SimpleChoice;
import uk.ac.ed.ph.jqtiplus.types.Identifier;
import uk.ac.ed.ph.jqtiplus.value.Orientation;
/**
*
* Initial date: 26.05.2015<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class MultipleChoiceEditorController extends FormBasicController {
private TextElement titleEl;
private RichTextElement textEl;
private SingleSelection shuffleEl, orientationEl, alignmentEl;
private FormLayoutContainer answersCont;
private final List<SimpleChoiceWrapper> choiceWrappers = new ArrayList<>();
private final VFSContainer itemContainer;
private int count = 0;
private final boolean restrictedEdit;
private final MultipleChoiceAssessmentItemBuilder itemBuilder;
private static final String[] yesnoKeys = new String[]{ "y", "n"};
private static final String[] alignmentKeys = new String[]{ "left", "right"};
private static final String[] layoutKeys = new String[]{ Orientation.VERTICAL.name(), Orientation.HORIZONTAL.name() };
public MultipleChoiceEditorController(UserRequest ureq, WindowControl wControl,
MultipleChoiceAssessmentItemBuilder itemBuilder,
File rootDirectory, VFSContainer rootContainer, File itemFile, boolean restrictedEdit) {
super(ureq, wControl, "simple_choices_editor");
setTranslator(Util.createPackageTranslator(AssessmentTestEditorController.class, getLocale()));
this.itemBuilder = itemBuilder;
this.restrictedEdit = restrictedEdit;
String relativePath = rootDirectory.toPath().relativize(itemFile.toPath().getParent()).toString();
itemContainer = (VFSContainer)rootContainer.resolve(relativePath);
initForm(ureq);
}
@Override
protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
FormLayoutContainer metadata = FormLayoutContainer.createDefaultFormLayout_2_10("metadata", getTranslator());
metadata.setFormContextHelp("Test editor QTI 2.1 in detail#details_testeditor_fragetypen_mc");
metadata.setRootForm(mainForm);
formLayout.add(metadata);
formLayout.add("metadata", metadata);
titleEl = uifactory.addTextElement("title", "form.imd.title", -1, itemBuilder.getTitle(), metadata);
titleEl.setElementCssClass("o_sel_assessment_item_title");
titleEl.setMandatory(true);
String description = itemBuilder.getQuestion();
textEl = uifactory.addRichTextElementForQTI21("desc", "form.imd.descr", description, 8, -1, itemContainer,
metadata, ureq.getUserSession(), getWindowControl());
//shuffle
String[] yesnoValues = new String[]{ translate("yes"), translate("no") };
shuffleEl = uifactory.addRadiosHorizontal("shuffle", "form.imd.shuffle", metadata, yesnoKeys, yesnoValues);
shuffleEl.setEnabled(!restrictedEdit);
if (itemBuilder.isShuffle()) {
shuffleEl.select("y", true);
} else {
shuffleEl.select("n", true);
}
//layout
String[] layoutValues = new String[]{ translate("form.imd.layout.vertical"), translate("form.imd.layout.horizontal") };
orientationEl = uifactory.addRadiosHorizontal("layout", "form.imd.layout", metadata, layoutKeys, layoutValues);
orientationEl.setEnabled(!restrictedEdit);
if (itemBuilder.getOrientation() == null || Orientation.VERTICAL.equals(itemBuilder.getOrientation())) {
orientationEl.select(Orientation.VERTICAL.name(), true);
} else {
orientationEl.select(Orientation.HORIZONTAL.name(), true);
}
//alignment
String[] alignmentValues = new String[]{ translate("form.imd.alignment.left"), translate("form.imd.alignment.right") };
alignmentEl = uifactory.addRadiosHorizontal("alignment", "form.imd.alignment", metadata, alignmentKeys, alignmentValues);
alignmentEl.setEnabled(!restrictedEdit);
if (itemBuilder.hasClassAttr(QTI21Constants.CHOICE_ALIGN_RIGHT)) {
alignmentEl.select(alignmentKeys[1], true);
} else {
alignmentEl.select(alignmentKeys[0], true);
}
//responses
String page = velocity_root + "/multiple_choices.html";
answersCont = FormLayoutContainer.createCustomFormLayout("answers", getTranslator(), page);
answersCont.setRootForm(mainForm);
formLayout.add(answersCont);
formLayout.add("answers", answersCont);
ChoiceInteraction interaction = itemBuilder.getChoiceInteraction();
if(interaction != null) {
List<SimpleChoice> choices = itemBuilder.getChoices();
for(SimpleChoice choice:choices) {
wrapAnswer(ureq, choice);
}
}
answersCont.contextPut("choices", choiceWrappers);
answersCont.contextPut("restrictedEdit", restrictedEdit);
recalculateUpDownLinks();
// Submit Button
FormLayoutContainer buttonsContainer = FormLayoutContainer.createDefaultFormLayout_2_10("buttons", getTranslator());
buttonsContainer.setElementCssClass("o_sel_choices_save");
buttonsContainer.setRootForm(mainForm);
formLayout.add(buttonsContainer);
formLayout.add("buttons", buttonsContainer);
uifactory.addFormSubmitButton("submit", buttonsContainer);
}
private void wrapAnswer(UserRequest ureq, SimpleChoice choice) {
String choiceContent = itemBuilder.getHtmlHelper().flowStaticString(choice.getFlowStatics());
String choiceId = "answer" + count++;
RichTextElement choiceEl = uifactory.addRichTextElementForQTI21(choiceId, "form.imd.answer", choiceContent, 8, -1, itemContainer,
answersCont, ureq.getUserSession(), getWindowControl());
choiceEl.setUserObject(choice);
answersCont.add("choiceId", choiceEl);
FormLink removeLink = uifactory.addFormLink("rm-".concat(choiceId), "rm", "", null, answersCont, Link.NONTRANSLATED);
removeLink.setIconLeftCSS("o_icon o_icon-lg o_icon_delete");
removeLink.setEnabled(!restrictedEdit);
answersCont.add(removeLink);
answersCont.add("rm-".concat(choiceId), removeLink);
FormLink addLink = uifactory.addFormLink("add-".concat(choiceId), "add", "", null, answersCont, Link.NONTRANSLATED);
addLink.setIconLeftCSS("o_icon o_icon-lg o_icon_add");
addLink.setEnabled(!restrictedEdit);
answersCont.add(addLink);
answersCont.add("add-".concat(choiceId), addLink);
FormLink upLink = uifactory.addFormLink("up-".concat(choiceId), "up", "", null, answersCont, Link.NONTRANSLATED);
upLink.setIconLeftCSS("o_icon o_icon-lg o_icon_move_up");
upLink.setEnabled(!restrictedEdit);
answersCont.add(upLink);
answersCont.add("up-".concat(choiceId), upLink);
FormLink downLink = uifactory.addFormLink("down-".concat(choiceId), "down", "", null, answersCont, Link.NONTRANSLATED);
downLink.setIconLeftCSS("o_icon o_icon-lg o_icon_move_down");
downLink.setEnabled(!restrictedEdit);
answersCont.add(downLink);
answersCont.add("down-".concat(choiceId), downLink);
choiceWrappers.add(new SimpleChoiceWrapper(choice, choiceEl, removeLink, addLink, upLink, downLink));
}
@Override
protected void doDispose() {
//
}
@Override
protected boolean validateFormLogic(UserRequest ureq) {
boolean allOk = true;
titleEl.clearError();
if(!StringHelper.containsNonWhitespace(titleEl.getValue())) {
titleEl.setErrorKey("form.legende.mandatory", null);
allOk &= false;
}
answersCont.clearError();
if(!restrictedEdit) {
String[] correctAnswers = ureq.getHttpReq().getParameterValues("correct");
if(correctAnswers == null || correctAnswers.length == 0) {
answersCont.setErrorKey("error.need.correct.answer", null);
allOk &= false;
}
}
return allOk & super.validateFormLogic(ureq);
}
@Override
protected void formOK(UserRequest ureq) {
//title
itemBuilder.setTitle(titleEl.getValue());
//question
String questionText = textEl.getRawValue();
itemBuilder.setQuestion(questionText);
if(!restrictedEdit) {
//correct response
String[] correctAnswers = ureq.getHttpReq().getParameterValues("correct");
List<Identifier> correctAnswerList = new ArrayList<>();
for(String correctAnswer:correctAnswers) {
correctAnswerList.add(Identifier.parseString(correctAnswer));
}
itemBuilder.setCorrectAnswers(correctAnswerList);
//shuffle
itemBuilder.setShuffle(shuffleEl.isOneSelected() && shuffleEl.isSelected(0));
//orientation
itemBuilder.setOrientation(Orientation.valueOf(orientationEl.getSelectedKey()));
//alignment
if(alignmentEl.isOneSelected() && alignmentEl.isSelected(1)) {
itemBuilder.addClass(QTI21Constants.CHOICE_ALIGN_RIGHT);
} else {
itemBuilder.removeClass(QTI21Constants.CHOICE_ALIGN_RIGHT);
}
}
//replace simple choices
List<SimpleChoice> choiceList = new ArrayList<>();
for(SimpleChoiceWrapper choiceWrapper:choiceWrappers) {
SimpleChoice choice = choiceWrapper.getSimpleChoice();
//text
String answer = choiceWrapper.getAnswer().getRawValue();
itemBuilder.getHtmlHelper().appendHtml(choice, answer);
choiceList.add(choice);
}
itemBuilder.setSimpleChoices(choiceList);
fireEvent(ureq, new AssessmentItemEvent(AssessmentItemEvent.ASSESSMENT_ITEM_CHANGED, itemBuilder.getAssessmentItem(), QTI21QuestionType.sc));
}
@Override
protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
if(source instanceof FormLink) {
FormLink button = (FormLink)source;
String cmd = button.getCmd();
if("rm".equals(cmd)) {
updateCorrectAnswers(ureq);
doRemoveSimpleChoice((SimpleChoiceWrapper)button.getUserObject());
} else if("add".equals(cmd)) {
updateCorrectAnswers(ureq);
doAddSimpleChoice(ureq);
} else if("up".equals(cmd)) {
updateCorrectAnswers(ureq);
doMoveSimpleChoiceUp((SimpleChoiceWrapper)button.getUserObject());
} else if("down".equals(cmd)) {
updateCorrectAnswers(ureq);
doMoveSimpleChoiceDown((SimpleChoiceWrapper)button.getUserObject());
}
}
super.formInnerEvent(ureq, source, event);
}
private void updateCorrectAnswers(UserRequest ureq) {
String[] correctAnswers = ureq.getHttpReq().getParameterValues("correct");
if(correctAnswers != null) {
List<Identifier> correctAnswerList = new ArrayList<>();
for(String correctAnswer:correctAnswers) {
correctAnswerList.add(Identifier.parseString(correctAnswer));
}
itemBuilder.setCorrectAnswers(correctAnswerList);
}
}
private void doAddSimpleChoice(UserRequest ureq) {
ChoiceInteraction interaction = itemBuilder.getChoiceInteraction();
SimpleChoice newChoice = new SimpleChoice(interaction);
newChoice.setIdentifier(IdentifierGenerator.newAsIdentifier("mc"));
P firstChoiceText = AssessmentItemFactory.getParagraph(newChoice, translate("new.answer"));
newChoice.getFlowStatics().add(firstChoiceText);
wrapAnswer(ureq, newChoice);
flc.setDirty(true);
}
private void doRemoveSimpleChoice(SimpleChoiceWrapper choiceWrapper) {
choiceWrappers.remove(choiceWrapper);
flc.setDirty(true);
}
private void doMoveSimpleChoiceUp(SimpleChoiceWrapper choiceWrapper) {
int index = choiceWrappers.indexOf(choiceWrapper) - 1;
if(index >= 0 && index < choiceWrappers.size()) {
choiceWrappers.remove(choiceWrapper);
choiceWrappers.add(index, choiceWrapper);
}
recalculateUpDownLinks();
flc.setDirty(true);
}
private void doMoveSimpleChoiceDown(SimpleChoiceWrapper choiceWrapper) {
int index = choiceWrappers.indexOf(choiceWrapper) + 1;
if(index > 0 && index < choiceWrappers.size()) {
choiceWrappers.remove(choiceWrapper);
choiceWrappers.add(index, choiceWrapper);
}
recalculateUpDownLinks();
flc.setDirty(true);
}
private void recalculateUpDownLinks() {
int numOfChoices = choiceWrappers.size();
for(int i=0; i<numOfChoices; i++) {
SimpleChoiceWrapper choiceWrapper = choiceWrappers.get(i);
choiceWrapper.getUp().setEnabled(i != 0);
choiceWrapper.getDown().setEnabled(i < (numOfChoices - 1));
}
}
public final class SimpleChoiceWrapper {
private final SimpleChoice choice;
private final RichTextElement answerEl;
private final FormLink removeLink, addLink, upLink, downLink;
private final Identifier choiceIdentifier;
public SimpleChoiceWrapper(SimpleChoice choice, RichTextElement answerEl,
FormLink removeLink, FormLink addLink, FormLink upLink, FormLink downLink) {
this.choice = choice;
this.choiceIdentifier = choice.getIdentifier();
this.answerEl = answerEl;
answerEl.setUserObject(this);
this.removeLink = removeLink;
removeLink.setUserObject(this);
this.addLink = addLink;
addLink.setUserObject(this);
this.upLink = upLink;
upLink.setUserObject(this);
this.downLink = downLink;
downLink.setUserObject(this);
}
public Identifier getIdentifier() {
return choiceIdentifier;
}
public String getIdentifierString() {
return choiceIdentifier.toString();
}
public boolean isCorrect() {
return itemBuilder.isCorrect(choice);
}
public SimpleChoice getSimpleChoice() {
return choice;
}
public RichTextElement getAnswer() {
return answerEl;
}
public FormLink getRemove() {
return removeLink;
}
public FormLink getAdd() {
return addLink;
}
public FormLink getUp() {
return upLink;
}
public FormLink getDown() {
return downLink;
}
}
}