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 a2d956b7f71a2772da71313875b2639f291cc2e1..af53e6b42a460d766a715c6847b4b504a028367c 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 @@ -325,10 +325,13 @@ public class AssessmentItemFactory { return responseDeclaration; } - public static ResponseDeclaration createHotspotCorrectResponseDeclaration(AssessmentItem assessmentItem, Identifier declarationId, List<Identifier> correctResponseIds) { + public static ResponseDeclaration createHotspotCorrectResponseDeclaration(AssessmentItem assessmentItem, Identifier declarationId, + List<Identifier> correctResponseIds, Cardinality cardinality) { ResponseDeclaration responseDeclaration = new ResponseDeclaration(assessmentItem); responseDeclaration.setIdentifier(declarationId); - if(correctResponseIds == null || correctResponseIds.size() == 0 || correctResponseIds.size() > 1) { + if(cardinality != null && (cardinality == Cardinality.SINGLE || cardinality == Cardinality.MULTIPLE)) { + responseDeclaration.setCardinality(cardinality); + } else if(correctResponseIds == null || correctResponseIds.size() == 0 || correctResponseIds.size() > 1) { responseDeclaration.setCardinality(Cardinality.MULTIPLE); } else { responseDeclaration.setCardinality(Cardinality.SINGLE); diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/HotspotAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/HotspotAssessmentItemBuilder.java index 32ccf6a7158c4cf96c74bad8e6f57e80fcd76638..f3c2882ea522aeebf78ecaffd732398ce9026330 100644 --- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/HotspotAssessmentItemBuilder.java +++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/HotspotAssessmentItemBuilder.java @@ -74,6 +74,7 @@ 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.Cardinality; import uk.ac.ed.ph.jqtiplus.value.IdentifierValue; import uk.ac.ed.ph.jqtiplus.value.SingleValue; @@ -86,6 +87,7 @@ import uk.ac.ed.ph.jqtiplus.value.SingleValue; public class HotspotAssessmentItemBuilder extends AssessmentItemBuilder implements ResponseIdentifierForFeedback { private String question; + private Cardinality cardinality; private Identifier responseIdentifier; private List<Identifier> correctAnswers; protected ScoreEvaluation scoreEvaluation; @@ -152,9 +154,12 @@ public class HotspotAssessmentItemBuilder extends AssessmentItemBuilder implemen if(hotspotInteraction != null) { ResponseDeclaration responseDeclaration = assessmentItem .getResponseDeclaration(hotspotInteraction.getResponseIdentifier()); - if(responseDeclaration != null && responseDeclaration.getCorrectResponse() != null) { - CorrectResponse correctResponse = responseDeclaration.getCorrectResponse(); - extractIdentifiersFromCorrectResponse(correctResponse, correctAnswers); + if(responseDeclaration != null) { + if(responseDeclaration.getCorrectResponse() != null) { + CorrectResponse correctResponse = responseDeclaration.getCorrectResponse(); + extractIdentifiersFromCorrectResponse(correctResponse, correctAnswers); + } + cardinality = responseDeclaration.getCardinality(); } } } @@ -188,6 +193,14 @@ public class HotspotAssessmentItemBuilder extends AssessmentItemBuilder implemen return responseIdentifier; } + public boolean isSingleChoice() { + return cardinality == Cardinality.SINGLE; + } + + public void setCardinality(Cardinality cardinality) { + this.cardinality = cardinality; + } + @Override public List<Answer> getAnswers() { List<HotspotChoice> hotspotChoices = getHotspotChoices(); @@ -345,7 +358,7 @@ public class HotspotAssessmentItemBuilder extends AssessmentItemBuilder implemen @Override protected void buildResponseAndOutcomeDeclarations() { ResponseDeclaration responseDeclaration = AssessmentItemFactory - .createHotspotCorrectResponseDeclaration(assessmentItem, responseIdentifier, correctAnswers); + .createHotspotCorrectResponseDeclaration(assessmentItem, responseIdentifier, correctAnswers, cardinality); if(scoreEvaluation == ScoreEvaluation.perAnswer) { AssessmentItemFactory.appendMapping(responseDeclaration, scoreMapping); } diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java index 8f4a4aa8ee8def609f01a2b3ddfd069c299e4be8..b77adac94dec3fa7d92726b41cfaa9a68b7be075 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectVelocityRenderDecorator.java @@ -55,6 +55,7 @@ import uk.ac.ed.ph.jqtiplus.node.item.interaction.GapMatchInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicAssociateInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicGapMatchInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.GraphicOrderInteraction; +import uk.ac.ed.ph.jqtiplus.node.item.interaction.HotspotInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.HottextInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.InlineChoiceInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction; @@ -249,6 +250,13 @@ public class AssessmentObjectVelocityRenderDecorator extends VelocityRenderDecor return false; } return sc; + } else if(interaction instanceof HotspotInteraction) { + HotspotInteraction hotspotInteraction = (HotspotInteraction)interaction; + ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(hotspotInteraction.getResponseIdentifier()); + if(responseDeclaration != null && responseDeclaration.hasCardinality(Cardinality.SINGLE)) { + return true; + } + return false; } return false; } diff --git a/src/main/java/org/olat/ims/qti21/ui/components/_content/hotspotInteraction.html b/src/main/java/org/olat/ims/qti21/ui/components/_content/hotspotInteraction.html index 429eea6d4f0413769193b73b74ba2cd2eea8648d..afc1290c450ebf8fe793f3f09c6ad664dd6de11c 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/_content/hotspotInteraction.html +++ b/src/main/java/org/olat/ims/qti21/ui/components/_content/hotspotInteraction.html @@ -2,6 +2,7 @@ #set($qtiContainerId = "oc_" + $responseIdentifier) #set($responseValue = $r.getResponseValue($interaction.responseIdentifier)) #set($isResponsive = $r.hasCssClass($interaction, "interaction-responsive")) +#set($singleChoice = $r.isSingleChoice($interaction)) <input name="qtiworks_presented_${responseIdentifier}" type="hidden" value="1"/> <div class="$localName"> @@ -44,6 +45,7 @@ responseIdentifier: '$responseIdentifier', formDispatchFieldId: '$r.formDispatchFieldId', maxChoices: $interaction.maxChoices, + singleChoice: $singleChoice, responseValue: '$r.toString($responseValue,",")', opened: $isItemSessionOpen }); 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 c954c1df36cfd8f7743e4873b179fc133dfb2ea0..50cd4c726c6b80ca10e5c4567fa1a4592332629b 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 @@ -26,6 +26,7 @@ editor.unkown.title=Unbekanntes interaction error.assessment.test=Die Datei konnte nicht gelesen werden. Sie ist entweder korrupt oder mit dem falschen Format gespeichert. error.cannot.create.section=Sie k\u00F6nnen hier keine Sektion erstellen. error.cannot.delete=Sie d\u00FCrfen diese Ressource nicht l\u00F6schen. +error.cardinality.answer=Single choice erlaubt nur eine korrekte Antwort. error.double=$org.olat.ims.qti21.ui\:error.double error.import.question=Die Frage konnte wegen eine unerwartete Fehler nicht importiert werden error.integer=$org.olat.ims.qti21.ui\:error.integer @@ -87,6 +88,7 @@ form.imd.answered.title=Titel form.imd.background=Hintergrund form.imd.background.resize=Bildgr\u00F6sse f\u00FCr das Web optimieren form.imd.background.resize.no=Nicht anpassen +form.imd.cardinality=Typ form.imd.condition=Bedingung(en) form.imd.correct.kprim=Richtig form.imd.correct.spots=Korrekte Spots @@ -177,6 +179,7 @@ min.choices=Min. Anzahl von m min.choices.unlimited=Nicht begrenzet min.score=Minimal erreichbare Punktzahl minute.short=m +MULTIPLE=Multiple choice new.answer=Neue Antwort new.circle=Kreis new.drawing=Zeichnen @@ -200,6 +203,7 @@ new.testpart=Test-Part new.upload=Datei hochladen preview=Vorschau preview.solution=Vorschau L\u00F6sung +SINGLE=Single choice time.limit.max=Zeitbeschr\u00E4nkung title.add=$org.olat.ims.qti.editor\:title.add tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy 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 1e4a26a9cbce4c03b0277aa6ff342eb20d5c18c0..863fbe589864ad0aca86f479695241894b15d665 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 @@ -27,6 +27,7 @@ editor.unkown.title=Unkown interaction error.assessment.test=The file cannot be interpreted. It seems corrupted or with the wrong format. error.cannot.create.section=A section cannot be created everywhere\! error.cannot.delete=You cannot delete this object. +error.cardinality.answer=Single choice allow only one correct answer. error.double=$org.olat.ims.qti21.ui\:error.double error.import.question=An unexpected error happens during import of a question. error.integer=$org.olat.ims.qti21.ui\:error.integer @@ -88,6 +89,7 @@ form.imd.answered.title=Title form.imd.background=Background form.imd.background.resize=Optimize an image size for the Web form.imd.background.resize.no=Don't change +form.imd.cardinality=Type form.imd.condition=Condition(s) form.imd.correct.kprim=True form.imd.correct.spots=Correct spots @@ -178,6 +180,7 @@ min.choices=Min. number of possible answers min.choices.unlimited=Not limited min.score=Min. score minute.short=m +MULTIPLE=Multiple choice new.answer=New answer new.circle=Circle new.drawing=Drawing @@ -203,6 +206,7 @@ preview=Preview preview.solution=Preview solution time.limit.max=Time limit title.add=$org.olat.ims.qti.editor\:title.add +SINGLE=Single choice tools.change.copy=$org.olat.ims.qti.editor\:tools.change.copy tools.change.delete=$org.olat.ims.qti.editor\:tools.change.delete tools.export.docx=$org.olat.ims.qti.editor\:tools.export.docx diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties index 15872870ef2e8017b42fdcf8c76981250ac636b6..a6556b3732cd7fa2b7973e41c5c6c3bc880119ff 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties +++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_fr.properties @@ -66,6 +66,7 @@ form.imd.answered.title=Titre form.imd.background=Image de fond form.imd.background.resize=Optimiser la taille de l'image pour le web form.imd.background.resize.no=Laisser inchang\u00E9 +form.imd.cardinality=Type form.imd.correct.kprim=Vrai form.imd.correctSolution.text=Solution correcte form.imd.correctSolution.text.word=$\:form.imd.correctSolution.text (seulement pour export Word) diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java index ee268dc31202821ddf0aa9676a59db72d30ed9a7..307f80227c5b441e99c3d1883d3c9e8f5aeb84dd 100644 --- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java +++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java @@ -69,6 +69,7 @@ import org.springframework.beans.factory.annotation.Autowired; import uk.ac.ed.ph.jqtiplus.node.expression.operator.Shape; import uk.ac.ed.ph.jqtiplus.node.item.interaction.graphic.HotspotChoice; import uk.ac.ed.ph.jqtiplus.types.Identifier; +import uk.ac.ed.ph.jqtiplus.value.Cardinality; /** * @@ -92,6 +93,7 @@ public class HotspotEditorController extends FormBasicController { private RichTextElement textEl; private FileElement backgroundEl; private SingleSelection resizeEl; + private SingleSelection cardinalityEl; private FormLayoutContainer hotspotsCont; private MultipleSelectionElement responsiveEl; private FormLink newCircleButton, newRectButton; @@ -143,6 +145,15 @@ public class HotspotEditorController extends FormBasicController { formLayout, ureq.getUserSession(), getWindowControl()); textEl.addActionListener(FormEvent.ONCLICK); + String[] cardinalityKeys = new String[] { Cardinality.SINGLE.name(), Cardinality.MULTIPLE.name() }; + String[] cardinalityValues = new String[] { translate(Cardinality.SINGLE.name()), translate(Cardinality.MULTIPLE.name()) }; + cardinalityEl = uifactory.addRadiosHorizontal("form.imd.cardinality", formLayout, cardinalityKeys, cardinalityValues); + if(itemBuilder.isSingleChoice()) { + cardinalityEl.select(cardinalityKeys[0], true); + } else { + cardinalityEl.select(cardinalityKeys[1], true); + } + responsiveEl = uifactory.addCheckboxesHorizontal("form.imd.responsive", formLayout, onKeys, new String[] {""}); responsiveEl.setHelpText(translate("form.imd.responsive.hint")); if(itemBuilder.isResponsive()) { @@ -240,6 +251,12 @@ public class HotspotEditorController extends FormBasicController { } } + cardinalityEl.clearError(); + if(cardinalityEl.isSelected(0) && correctHotspotsEl.getSelectedKeys().size() > 1) { + cardinalityEl.setErrorKey("error.cardinality.answer", null); + allOk &= false; + } + return allOk & super.validateFormLogic(ureq); } @@ -418,6 +435,11 @@ public class HotspotEditorController extends FormBasicController { objectImg = initialBackgroundImage; } + if(cardinalityEl.isOneSelected()) { + String selectedCardinality = cardinalityEl.getSelectedKey(); + itemBuilder.setCardinality(Cardinality.valueOf(selectedCardinality)); + } + boolean updateHotspot = true; if(objectImg != null) { diff --git a/src/main/webapp/static/js/jquery/qti/jquery.hotspot.js b/src/main/webapp/static/js/jquery/qti/jquery.hotspot.js index 2d35266e6cea7bb7968ad98d7c8c012d0eefe8d6..d7737fc0fb93b82e02a32f1424e45ed952492cdb 100644 --- a/src/main/webapp/static/js/jquery/qti/jquery.hotspot.js +++ b/src/main/webapp/static/js/jquery/qti/jquery.hotspot.js @@ -1,117 +1,129 @@ (function ($) { - $.fn.hotspotInteraction = function(options) { - var settings = $.extend({ - responseIdentifier: null, - formDispatchFieldId: null, - maxChoices: 1, - responseValue: null, - opened: false - }, options ); - - try { - if(!(typeof settings.responseValue === "undefined") && settings.responseValue.length > 0) { - drawHotspotAreas(this, settings); - } - if(settings.opened) { - hotspots(this, settings); - } - } catch(e) { - if(window.console) console.log(e); - } - return this; - }; - - function drawHotspotAreas($obj, settings) { - var containerId = $obj.attr('id'); - var divContainer = jQuery('#' + containerId); - - var areaIds = settings.responseValue.split(','); - for(i=areaIds.length; i-->0; ) { - var areaEl = jQuery('#ac_' + settings.responseIdentifier + '_' + areaIds[i]); - var data = areaEl.data('maphilight') || {}; - data.selectedOn = true; - colorData(data); - areaEl.data('maphilight', data).trigger('alwaysOn.maphilight'); - - var inputElement = jQuery('<input type="hidden"/>') + $.fn.hotspotInteraction = function(options) { + var settings = $.extend({ + responseIdentifier: null, + formDispatchFieldId: null, + maxChoices: 1, + singleChoice: false, + responseValue: null, + opened: false + }, options ); + + try { + if(!(typeof settings.responseValue === "undefined") && settings.responseValue.length > 0) { + drawHotspotAreas(this, settings); + } + if(settings.opened) { + hotspots(this, settings); + } + } catch(e) { + if(window.console) console.log(e); + } + return this; + }; + + function drawHotspotAreas($obj, settings) { + var containerId = $obj.attr('id'); + var divContainer = jQuery('#' + containerId); + + var areaIds = settings.responseValue.split(','); + for(i=areaIds.length; i-->0; ) { + var areaEl = jQuery('#ac_' + settings.responseIdentifier + '_' + areaIds[i]); + var data = areaEl.data('maphilight') || {}; + data.selectedOn = true; + colorData(data); + areaEl.data('maphilight', data).trigger('alwaysOn.maphilight'); + + var inputElement = jQuery('<input type="hidden"/>') .attr('name', 'qtiworks_response_' + settings.responseIdentifier) .attr('value', areaEl.data('qti-id')); - divContainer.append(inputElement); - } - } - - function hotspots($obj, settings) { - var containerId = $obj.attr('id'); - jQuery('#' + containerId + " map area").each(function(index, el) { - jQuery(el).on('click', function() { - clickHotspotArea(this, containerId, settings.responseIdentifier, settings.maxChoices); - }); - }) - }; + divContainer.append(inputElement); + } + } + + function hotspots($obj, settings) { + var containerId = $obj.attr('id'); + jQuery('#' + containerId + " map area").each(function(index, el) { + jQuery(el).on('click', function() { + clickHotspotArea(this, containerId, settings.responseIdentifier, settings.maxChoices, settings.singleChoice); + }); + }) + }; - function clickHotspotArea(spot, containerId, responseIdentifier, maxChoices) { - var areaEl = jQuery(spot); - var data = areaEl.data('maphilight') || {}; - if((typeof data.selectedOn === "undefined") || !data.selectedOn) { - var numOfChoices = maxChoices; - if(numOfChoices > 0) { - var countChoices = 0; - jQuery("area", "map[name='" + containerId + "_map']").each(function(index, el) { - var cData = jQuery(el).data('maphilight') || {}; - if(cData.selectedOn) { - countChoices++; - } - }); - if(countChoices >= numOfChoices) { - return false; - } - } - } - - if(typeof data.selectedOn === "undefined") { - data.selectedOn = true; - } else { - data.selectedOn = !data.selectedOn; - } - colorData(data); - areaEl.data('maphilight', data).trigger('alwaysOn.maphilight'); + function clickHotspotArea(spot, containerId, responseIdentifier, maxChoices, singleChoice) { + var areaEl = jQuery(spot); + var data = areaEl.data('maphilight') || {}; + if((typeof data.selectedOn === "undefined") || !data.selectedOn) { + if(singleChoice) { + jQuery("area", "map[name='" + containerId + "_map']").each(function(index, el) { + var cData = jQuery(el).data('maphilight') || {}; + if(cData.selectedOn) { + cData.selectedOn = false; + colorData(cData); + jQuery(el).data('maphilight', cData).trigger('alwaysOn.maphilight'); + } + }); + } + + var numOfChoices = maxChoices; + if(numOfChoices > 0) { + var countChoices = 0; + jQuery("area", "map[name='" + containerId + "_map']").each(function(index, el) { + var cData = jQuery(el).data('maphilight') || {}; + if(cData.selectedOn) { + countChoices++; + } + }); + if(countChoices >= numOfChoices) { + return false; + } + } + } + + if(typeof data.selectedOn === "undefined") { + data.selectedOn = true; + } else { + data.selectedOn = !data.selectedOn; + } + colorData(data); + areaEl.data('maphilight', data).trigger('alwaysOn.maphilight'); - var divContainer = jQuery('#' + containerId); - divContainer.find("input[type='hidden']").remove(); - jQuery("area", "map[name='" + containerId + "_map']").each(function(index, el) { - var cAreaEl = jQuery(el); - var cData = cAreaEl.data('maphilight') || {}; - if(cData.selectedOn) { - var inputElement = jQuery('<input type="hidden"/>') - .attr('name', 'qtiworks_response_' + responseIdentifier) - .attr('value', cAreaEl.data('qti-id')); - divContainer.append(inputElement); - } - }); - }; - - /* - * Color the data based on the selectedOn flag - */ - function colorData(data) { - if(data.selectedOn) { - data.fillColor = '0000ff'; - data.fillOpacity = 0.5; - data.strokeColor = '0000ff'; - data.strokeOpacity = 1; - data.shadow = true; - data.shadowX = 0; - data.shadowY = 0; - data.shadowRadius = 7; - data.shadowColor = '000000'; - data.shadowOpacity = 0.8; - data.shadowPosition = 'outside'; - } else { + var divContainer = jQuery('#' + containerId); + divContainer.find("input[type='hidden']").remove(); + jQuery("area", "map[name='" + containerId + "_map']").each(function(index, el) { + var cAreaEl = jQuery(el); + var cData = cAreaEl.data('maphilight') || {}; + if(cData.selectedOn) { + var inputElement = jQuery('<input type="hidden"/>') + .attr('name', 'qtiworks_response_' + responseIdentifier) + .attr('value', cAreaEl.data('qti-id')); + divContainer.append(inputElement); + } + }); + }; + + /* + * Color the data based on the selectedOn flag + */ + function colorData(data) { + if(data.selectedOn) { + data.fillColor = '0000ff'; + data.fillOpacity = 0.5; + data.strokeColor = '0000ff'; + data.strokeOpacity = 1; + data.shadow = true; + data.shadowX = 0; + data.shadowY = 0; + data.shadowRadius = 7; + data.shadowColor = '000000'; + data.shadowOpacity = 0.8; + data.shadowPosition = 'outside'; + } else { data.fillColor = 'bbbbbb'; - data.fillOpacity = 0.5; - data.strokeColor = '666666'; - data.strokeOpacity = 0.8; - data.shadow = false; - } - } + data.fillOpacity = 0.5; + data.strokeColor = '666666'; + data.strokeOpacity = 0.8; + data.shadow = false; + } + } }( jQuery ));