/** * <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.components; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.checkJavaScript; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.contentAsString; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.exists; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.extractIterableElement; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.extractMathsContentPmathml; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.extractRecordFieldValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.extractResponseInputAt; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.extractSingleCardinalityResponseInput; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getAtClass; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getCardinalitySize; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getHtmlAttributeValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getOutcomeValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getResponseDeclaration; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getResponseInput; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getResponseValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.getTemplateValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isBadResponse; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isInvalidResponse; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isMathsContentValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isMultipleCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isNullValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isOrderedCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isRecordCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isSingleCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isTemplateDeclarationAMathVariable; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.isVisible; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.renderMultipleCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.renderOrderedCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.renderRecordCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.renderSingleCardinalityValue; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.valueContains; import java.io.Reader; import java.io.StringReader; import java.net.URI; import java.util.ArrayList; import java.util.List; import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.velocity.VelocityContext; import org.apache.velocity.context.Context; import org.olat.core.CoreSpringFactory; import org.olat.core.gui.components.DefaultComponentRenderer; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormUIFactory; import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.impl.Form; import org.olat.core.gui.components.form.flexible.impl.FormJSHelper; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.render.RenderResult; import org.olat.core.gui.render.Renderer; import org.olat.core.gui.render.StringOutput; import org.olat.core.gui.render.StringOutputPool; import org.olat.core.gui.render.URLBuilder; import org.olat.core.gui.render.velocity.VelocityHelper; import org.olat.core.gui.translator.Translator; import org.olat.core.helpers.Settings; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.CodeHelper; import org.olat.core.util.StringHelper; import org.olat.core.util.Util; import org.olat.ims.qti21.QTI21Constants; import org.olat.ims.qti21.QTI21Module; import org.olat.ims.qti21.QTI21Service; import org.olat.ims.qti21.XmlUtilities; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; import uk.ac.ed.ph.jqtiplus.attribute.Attribute; import uk.ac.ed.ph.jqtiplus.attribute.AttributeList; import uk.ac.ed.ph.jqtiplus.attribute.ForeignAttribute; import uk.ac.ed.ph.jqtiplus.attribute.value.IntegerAttribute; import uk.ac.ed.ph.jqtiplus.attribute.value.StringMultipleAttribute; import uk.ac.ed.ph.jqtiplus.node.ForeignElement; import uk.ac.ed.ph.jqtiplus.node.QtiNode; import uk.ac.ed.ph.jqtiplus.node.content.InfoControl; import uk.ac.ed.ph.jqtiplus.node.content.basic.AtomicBlock; import uk.ac.ed.ph.jqtiplus.node.content.basic.Block; import uk.ac.ed.ph.jqtiplus.node.content.basic.Flow; import uk.ac.ed.ph.jqtiplus.node.content.basic.Inline; import uk.ac.ed.ph.jqtiplus.node.content.basic.SimpleBlock; import uk.ac.ed.ph.jqtiplus.node.content.basic.SimpleInline; import uk.ac.ed.ph.jqtiplus.node.content.basic.TextRun; import uk.ac.ed.ph.jqtiplus.node.content.mathml.Math; import uk.ac.ed.ph.jqtiplus.node.content.template.TemplateBlock; import uk.ac.ed.ph.jqtiplus.node.content.template.TemplateInline; import uk.ac.ed.ph.jqtiplus.node.content.variable.FeedbackBlock; import uk.ac.ed.ph.jqtiplus.node.content.variable.FeedbackInline; import uk.ac.ed.ph.jqtiplus.node.content.variable.PrintedVariable; import uk.ac.ed.ph.jqtiplus.node.content.variable.RubricBlock; import uk.ac.ed.ph.jqtiplus.node.content.variable.TextOrVariable; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.hypertext.A; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.image.Img; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.list.Dd; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.list.Dl; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.list.DlElement; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.list.Dt; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.list.Li; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.list.Ol; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.list.Ul; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.object.Object; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.Col; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.Colgroup; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.Table; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.TableCell; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.Tbody; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.Tfoot; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.Thead; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.table.Tr; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.Br; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.Div; import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.Span; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.ModalFeedback; import uk.ac.ed.ph.jqtiplus.node.item.interaction.AssociateInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.ChoiceInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.CustomInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.DrawingInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.EndAttemptInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.ExtendedTextInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.FlowInteraction; 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.MatchInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.MediaInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.OrderInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.PositionObjectInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.SelectPointInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.SliderInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.TextEntryInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.UploadInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.content.Gap; import uk.ac.ed.ph.jqtiplus.node.item.interaction.content.Hottext; import uk.ac.ed.ph.jqtiplus.node.item.interaction.content.PositionObjectStage; import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration; import uk.ac.ed.ph.jqtiplus.node.shared.VariableDeclaration; import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode; import uk.ac.ed.ph.jqtiplus.node.test.TestPart; import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem; import uk.ac.ed.ph.jqtiplus.running.TestSessionController; import uk.ac.ed.ph.jqtiplus.state.ItemSessionState; import uk.ac.ed.ph.jqtiplus.types.Identifier; import uk.ac.ed.ph.jqtiplus.types.ResponseData; import uk.ac.ed.ph.jqtiplus.utils.QueryUtils; 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; import uk.ac.ed.ph.jqtiplus.value.Value; import uk.ac.ed.ph.jqtiplus.xmlutils.xslt.XsltStylesheetManager; import uk.ac.ed.ph.qtiworks.mathassess.MathEntryInteraction; /** * * Initial date: 21.09.2015<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public abstract class AssessmentObjectComponentRenderer extends DefaultComponentRenderer { private static final OLog log = Tracing.createLoggerFor(AssessmentObjectComponentRenderer.class); private static final String velocity_root = Util.getPackageVelocityRoot(AssessmentObjectComponentRenderer.class); private static final URI ctopXsltUri = URI.create("classpath:/org/olat/ims/qti21/ui/components/_content/ctop.xsl"); protected void renderExploded(StringOutput sb, Translator translator) { sb.append("<div class='o_error'>").append(translator.translate("exploded.msg")).append("</div>"); } protected void renderTerminated(StringOutput sb, Translator translator) { sb.append("<div class='o_info o_sel_assessment_test_terminated'>").append(translator.translate("terminated.msg")).append("</div>"); } protected void renderItemStatus(StringOutput sb, ItemSessionState itemSessionState, RenderingRequest options, Translator translator) { if(options != null && options.isSolutionMode()) { renderItemStatusMessage("review", "assessment.item.status.modelSolution", sb, translator); } else if(options != null && options.isReviewMode()) { renderItemReviewStatus(sb, itemSessionState, translator); } else if(itemSessionState.getEndTime() != null) { renderItemStatusMessage("ended", "assessment.item.status.finished", sb, translator); } else if(!(itemSessionState.getUnboundResponseIdentifiers().isEmpty() && itemSessionState.getInvalidResponseIdentifiers().isEmpty())) { renderItemStatusMessage("invalid", "assessment.item.status.needsAttention", sb, translator); } else if(itemSessionState.isResponded()) { if(itemSessionState.getUncommittedResponseValues().size() > 0) { renderItemStatusMessage("notAnswered", "assessment.item.status.notAnswered", sb, translator); } else { renderItemStatusMessage("answered", "assessment.item.status.answered", sb, translator); } } else if(itemSessionState.getEntryTime() != null) { renderItemStatusMessage("notAnswered", "assessment.item.status.notAnswered", sb, translator); } else { renderItemStatusMessage("notPresented", "assessment.item.status.notSeen", sb, translator); } } protected void renderItemReviewStatus(StringOutput sb, ItemSessionState itemSessionState, Translator translator) { if(!(itemSessionState.getUnboundResponseIdentifiers().isEmpty() && itemSessionState.getInvalidResponseIdentifiers().isEmpty())) { renderItemStatusMessage("reviewInvalid", "assessment.item.status.reviewInvalidAnswer", sb, translator); } else if(itemSessionState.isResponded()) { renderItemStatusMessage("review", "assessment.item.status.review", sb, translator); } else if(itemSessionState.getEntryTime() != null) { renderItemStatusMessage("reviewNotAnswered", "assessment.item.status.reviewNotAnswered", sb, translator); } else { renderItemStatusMessage("reviewNotSeen", "assessment.item.status.reviewNotSeen", sb, translator); } // missing? see AssessmentTestComponentRenderer // buildRenderStatus("reviewNotAllowed", "assessment.item.status.reviewNot", sb, translator); } private void renderItemStatusMessage(String status, String i18nKey, StringOutput sb, Translator translator) { String title = translator.translate(i18nKey); sb.append("<span class='o_assessmentitem_status ").append(status).append(" ' title=\"").append(StringEscapeUtils.escapeHtml(title)) .append("\"><i class='o_icon o_icon-fw o_icon_qti_").append(status).append("'> </i><span>").append(title).append("</span></span>"); } protected void renderTestItemModalFeedback(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, URLBuilder ubu, Translator translator) { List<ModalFeedback> modalFeedbacks = new ArrayList<>(); AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); for(ModalFeedback modalFeedback:assessmentItem.getModalFeedbacks()) { if(component.isFeedback(modalFeedback, itemSessionState)) { modalFeedbacks.add(modalFeedback); } } if(modalFeedbacks.size() > 0) { sb.append("<div class='modalFeedback'>"); for(ModalFeedback modalFeedback:modalFeedbacks) { Identifier outcomeIdentifier = modalFeedback.getOutcomeIdentifier(); if(QTI21Constants.FEEDBACKMODAL_IDENTIFIER.equals(outcomeIdentifier)) { renderTestItemModalFeedback_feedbackModal(renderer, sb, modalFeedback, component, resolvedAssessmentItem, itemSessionState, ubu, translator); } else if(QTI21Constants.CORRECT_SOLUTION_IDENTIFIER.equals(outcomeIdentifier)) { renderTestItemModalFeedback_correctSolution(renderer, sb, modalFeedback, component, resolvedAssessmentItem, itemSessionState, ubu, translator); } else { renderTestItemModalFeedback_standard(renderer, sb, modalFeedback, component, resolvedAssessmentItem, itemSessionState, ubu, translator); } } sb.append("</div>"); } } protected void renderTestItemModalFeedback_correctSolution(AssessmentRenderer renderer, StringOutput sb, ModalFeedback modalFeedback, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, URLBuilder ubu, Translator translator) { sb.append("<div class='modalFeedback o_togglebox_wrapper o_block clearfix'>"); Attribute<?> title = modalFeedback.getAttributes().get("title"); String feedbackTitle = null; if(title != null && title.getValue() != null) { feedbackTitle = title.getValue().toString(); } if(!StringHelper.containsNonWhitespace(feedbackTitle)) { feedbackTitle = translator.translate("correct.solution"); } sb.append("<h4><a href='#modal-correct-solution' data-toggle='collapse' data-target='#modal-correct-solution' class=\"o_opener\" onclick=\"jQuery(this).toggleClass('o_in'); return false;\"><i class='o_icon o_icon-fw o_icon-lg'> </i> ").append(StringHelper.escapeHtml(feedbackTitle)).append("</a></h4>"); sb.append("<div id='modal-correct-solution' class='collapse'><div class='o_togglebox_content clearfix'>"); modalFeedback.getFlowStatics().forEach((flow) -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, flow, ubu, translator)); sb.append("</div></div></div>"); /* <div class="o_togglebox_wrapper o_block"> <a href="#$uid" data-toggle="collapse" data-target="#${uid}" class="o_opener" onclick="jQuery(this).toggleClass('o_in'); return false;"> <i class="o_icon o_icon-fw o_icon-lg"></i> $title </a> <div id="$uid" class="collapse"> <div class="o_togglebox_content clearfix"> */ } /** * Render the feedback modal generated by OpenOLAT editor. * * @param renderer * @param sb * @param modalFeedback * @param component * @param resolvedAssessmentItem * @param itemSessionState * @param ubu * @param translator */ protected void renderTestItemModalFeedback_feedbackModal(AssessmentRenderer renderer, StringOutput sb, ModalFeedback modalFeedback, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, URLBuilder ubu, Translator translator) { sb.append("<div class='modalFeedback o_info clearfix"); Value feedbackBasic = itemSessionState.getOutcomeValue(QTI21Constants.FEEDBACKBASIC_IDENTIFIER); if(feedbackBasic != null && feedbackBasic.hasBaseType(BaseType.IDENTIFIER) && feedbackBasic instanceof IdentifierValue) { IdentifierValue identifierValue = (IdentifierValue)feedbackBasic; if(QTI21Constants.CORRECT_IDENTIFIER_VALUE.equals(identifierValue)) { sb.append(" o_correct_modal_feedback"); } else if(QTI21Constants.INCORRECT_IDENTIFIER_VALUE.equals(identifierValue)) { sb.append(" o_incorrect_modal_feedback"); } else if(QTI21Constants.EMPTY_IDENTIFIER_VALUE.equals(identifierValue)) { sb.append(" o_empty_modal_feedback"); } } sb.append("'>"); Attribute<?> title = modalFeedback.getAttributes().get("title"); if(title != null && title.getValue() != null) { String feedbackTitle = title.getValue().toString(); if(StringHelper.containsNonWhitespace(feedbackTitle)) { sb.append("<h4>"); if(modalFeedback.getIdentifier() != null && QTI21Constants.HINT_IDENTIFIER.equals(modalFeedback.getIdentifier())) { sb.append("<i class='o_icon o_icon_help'> </i> "); } sb.append(StringHelper.escapeHtml(feedbackTitle)).append("</h4>"); } } modalFeedback.getFlowStatics().forEach((flow) -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, flow, ubu, translator)); sb.append("</div>"); } /** * Render the standard feedback in an info box. * * @param renderer * @param sb * @param modalFeedback * @param component * @param resolvedAssessmentItem * @param itemSessionState * @param ubu * @param translator */ protected void renderTestItemModalFeedback_standard(AssessmentRenderer renderer, StringOutput sb, ModalFeedback modalFeedback, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, URLBuilder ubu, Translator translator) { sb.append("<div class='modalFeedback o_info clearfix'>"); Attribute<?> title = modalFeedback.getAttributes().get("title"); if(title != null && title.getValue() != null) { String feedbackTitle = title.getValue().toString(); if(StringHelper.containsNonWhitespace(feedbackTitle)) { sb.append("<h4>"); if(modalFeedback.getIdentifier() != null && QTI21Constants.HINT_IDENTIFIER.equals(modalFeedback.getIdentifier())) { sb.append("<i class='o_icon o_icon_help'> </i> "); } sb.append(StringHelper.escapeHtml(feedbackTitle)).append("</h4>"); } } modalFeedback.getFlowStatics().forEach((flow) -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, flow, ubu, translator)); sb.append("</div>"); } public void renderFlow(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Flow flow, URLBuilder ubu, Translator translator) { if(flow instanceof Block) { renderBlock(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (Block)flow, ubu, translator); } else if(flow instanceof Inline) { renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (Inline)flow, ubu, translator); } else { log.error("What is it for a flow static object: " + flow); } } public void renderTextOrVariable(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, TextOrVariable textOrVariable) { if(textOrVariable instanceof PrintedVariable) { renderPrintedVariable(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (PrintedVariable)textOrVariable); } else if(textOrVariable instanceof TextRun) { sb.append(((TextRun)textOrVariable).getTextContent()); } else { log.error("What is it for a textOrVariable object: " + textOrVariable); } } public void renderBlock(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Block block, URLBuilder ubu, Translator translator) { switch(block.getQtiClassName()) { case AssociateInteraction.QTI_CLASS_NAME: case ChoiceInteraction.QTI_CLASS_NAME: case DrawingInteraction.QTI_CLASS_NAME: case ExtendedTextInteraction.QTI_CLASS_NAME: case GapMatchInteraction.QTI_CLASS_NAME: case GraphicAssociateInteraction.QTI_CLASS_NAME: case GraphicGapMatchInteraction.QTI_CLASS_NAME: case GraphicOrderInteraction.QTI_CLASS_NAME: case HotspotInteraction.QTI_CLASS_NAME: case SelectPointInteraction.QTI_CLASS_NAME: case HottextInteraction.QTI_CLASS_NAME: case MatchInteraction.QTI_CLASS_NAME: case MediaInteraction.QTI_CLASS_NAME: case OrderInteraction.QTI_CLASS_NAME: case PositionObjectInteraction.QTI_CLASS_NAME: case SliderInteraction.QTI_CLASS_NAME: case UploadInteraction.QTI_CLASS_NAME: { renderInteraction(renderer, sb, (FlowInteraction)block, resolvedAssessmentItem, itemSessionState, component, ubu, translator); break; } case CustomInteraction.QTI_CLASS_NAME: { renderCustomInteraction(renderer, sb, (CustomInteraction<?>)block, resolvedAssessmentItem, itemSessionState, component, ubu, translator); break; } case PositionObjectStage.QTI_CLASS_NAME: { renderPositionObjectStage(renderer, sb, (PositionObjectStage)block, resolvedAssessmentItem, itemSessionState, component, ubu, translator); break; } case TemplateBlock.QTI_CLASS_NAME: break;//never rendered case InfoControl.QTI_CLASS_NAME: { renderInfoControl(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (InfoControl)block, ubu, translator); break; } case FeedbackBlock.QTI_CLASS_NAME: { FeedbackBlock feedbackBlock = (FeedbackBlock)block; if(component.isFeedback(feedbackBlock, itemSessionState)) { sb.append("<div class='o_info feedbackBlock '").append(getAtClass(feedbackBlock)).append(">"); feedbackBlock.getBlocks().forEach((child) -> renderBlock(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); sb.append("</div>"); } break; } case RubricBlock.QTI_CLASS_NAME: break; //never rendered automatically case Math.QTI_CLASS_NAME: { renderMath(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (Math)block); break; } case Div.QTI_CLASS_NAME: renderStartHtmlTag(sb, component, resolvedAssessmentItem, block, null); ((Div)block).getFlows().forEach((flow) -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, flow, ubu, translator)); renderEndTag(sb, block); break; case Ul.QTI_CLASS_NAME: renderStartHtmlTag(sb, component, resolvedAssessmentItem, block, null); ((Ul)block).getLis().forEach((li) -> renderLi(renderer, sb, component, resolvedAssessmentItem, itemSessionState, li, ubu, translator)); renderEndTag(sb, block); break; case Ol.QTI_CLASS_NAME: renderStartHtmlTag(sb, component, resolvedAssessmentItem, block, null); ((Ol)block).getLis().forEach((li) -> renderLi(renderer, sb, component, resolvedAssessmentItem, itemSessionState, li, ubu, translator)); renderEndTag(sb, block); break; case Dl.QTI_CLASS_NAME: renderStartHtmlTag(sb, component, resolvedAssessmentItem, block, null); ((Dl)block).getDlElements().forEach((dlElement) -> renderDlElement(renderer, sb, component, resolvedAssessmentItem, itemSessionState, dlElement, ubu, translator)); renderEndTag(sb, block); break; case Table.QTI_CLASS_NAME: renderTable(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (Table)block, ubu, translator); break; case Object.QTI_CLASS_NAME: System.out.println("1"); break; default: { renderStartHtmlTag(sb, component, resolvedAssessmentItem, block, null); if(block instanceof AtomicBlock) { AtomicBlock atomicBlock = (AtomicBlock)block; atomicBlock.getInlines().forEach((child) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); } else if(block instanceof SimpleBlock) { SimpleBlock simpleBlock = (SimpleBlock)block; simpleBlock.getBlocks().forEach((child) -> renderBlock(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); } renderEndTag(sb, block); } } } public void renderTable(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Table table, URLBuilder ubu, Translator translator) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, table, null); table.getColgroups().forEach(colgroup -> renderColgroup(sb, component, resolvedAssessmentItem, colgroup)); Thead thead = table.getThead(); if(thead != null) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, thead, null); thead.getTrs().forEach(tr -> renderTr(renderer, sb, component, resolvedAssessmentItem, itemSessionState, tr, ubu, translator)); renderEndTag(sb, thead); } List<Tbody> tbodies = table.getTbodys(); for(Tbody tbody:tbodies) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, tbody, null); tbody.getTrs().forEach(tr -> renderTr(renderer, sb, component, resolvedAssessmentItem, itemSessionState, tr, ubu, translator)); renderEndTag(sb, tbody); } Tfoot tfoot = table.getTfoot(); if(tfoot != null) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, tfoot, null); tfoot.getTrs().forEach(tr -> renderTr(renderer, sb, component, resolvedAssessmentItem, itemSessionState, tr, ubu, translator)); renderEndTag(sb, tfoot); } renderEndTag(sb, table); } public void renderColgroup(StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, Colgroup colGroup) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, colGroup, null); colGroup.getCols().forEach(col -> renderCol(sb, component, resolvedAssessmentItem, col)); renderEndTag(sb, colGroup); } public void renderCol(StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, Col col) { sb.append("<").append(col.getQtiClassName()); renderHtmlTagAttributes(sb, component, resolvedAssessmentItem, col, null); IntegerAttribute spanAttr = col.getAttributes().getIntegerAttribute(Col.ATTR_SPAN_NAME); if(spanAttr.getComputedValue() != null) { sb.append(" span=\"").append(spanAttr.getComputedNonNullValue()).append("\""); } sb.append(">"); renderEndTag(sb, col); } public void renderTr(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Tr tr, URLBuilder ubu, Translator translator) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, tr, null); tr.getTableCells().forEach(cell -> renderTableCell(renderer, sb, component, resolvedAssessmentItem, itemSessionState, cell, ubu, translator)); renderEndTag(sb, tr); } public void renderTableCell(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, TableCell cell, URLBuilder ubu, Translator translator) { sb.append("<").append(cell.getQtiClassName()); renderHtmlTagAttributes(sb, component, resolvedAssessmentItem, cell, null); IntegerAttribute colSpanAttr = cell.getAttributes().getIntegerAttribute(TableCell.ATTR_COLSPAN_NAME); if(colSpanAttr.getComputedValue() != null) { sb.append(" colspan=\"").append(colSpanAttr.getComputedNonNullValue()).append("\""); } IntegerAttribute rowSpanAttr = cell.getAttributes().getIntegerAttribute(TableCell.ATTR_ROWSPAN_NAME); if(rowSpanAttr.getComputedValue() != null) { sb.append(" rowspan=\"").append(rowSpanAttr.getComputedNonNullValue()).append("\""); } sb.append(">"); cell.getChildren().forEach(child -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); renderEndTag(sb, cell); } public void renderLi(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Li li, URLBuilder ubu, Translator translator) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, li, null); li.getFlows().forEach((flow) -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, flow, ubu, translator)); renderEndTag(sb, li); } public void renderDlElement(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, DlElement dlElement, URLBuilder ubu, Translator translator) { renderStartHtmlTag(sb, component, resolvedAssessmentItem, dlElement, null); switch(dlElement.getQtiClassName()) { case Dt.QTI_CLASS_NAME: ((Dt)dlElement).getInlines().forEach((inline) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, inline, ubu, translator)); break; case Dd.QTI_CLASS_NAME: ((Dd)dlElement).getFlows().forEach((flow) -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, flow, ubu, translator)); break; default: //ignore other type break; } renderEndTag(sb, dlElement); } public void renderInline(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Inline inline, URLBuilder ubu, Translator translator) { switch(inline.getQtiClassName()) { case EndAttemptInteraction.QTI_CLASS_NAME: { renderEndAttemptInteraction(renderer, sb, (EndAttemptInteraction)inline, itemSessionState, component, ubu, translator); break; } case InlineChoiceInteraction.QTI_CLASS_NAME: case TextEntryInteraction.QTI_CLASS_NAME: { renderInteraction(renderer, sb, (FlowInteraction)inline, resolvedAssessmentItem, itemSessionState, component, ubu, translator); break; } case Hottext.QTI_CLASS_NAME: { renderHottext(renderer, sb, resolvedAssessmentItem, itemSessionState, (Hottext)inline, component, ubu, translator); break; } case Gap.QTI_CLASS_NAME: { renderGap(sb, (Gap)inline, itemSessionState, component); break; } case PrintedVariable.QTI_CLASS_NAME: { renderPrintedVariable(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (PrintedVariable)inline); break; } case TemplateInline.QTI_CLASS_NAME: break; //not part of the item body case FeedbackInline.QTI_CLASS_NAME: { FeedbackInline feedbackInline = (FeedbackInline)inline; if(component.isFeedback(feedbackInline, itemSessionState)) { sb.append("<span class='feedbackInline ").append(getAtClass(feedbackInline)).append("'>"); feedbackInline.getInlines().forEach((child) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); sb.append("</span>"); } break; } case TextRun.DISPLAY_NAME: { sb.append(((TextRun)inline).getTextContent()); break; } case Math.QTI_CLASS_NAME: { renderMath(renderer, sb, component, resolvedAssessmentItem, itemSessionState, (Math)inline); break; } case Img.QTI_CLASS_NAME: { renderHtmlTag(sb, component, resolvedAssessmentItem, inline, null); break; } case Br.QTI_CLASS_NAME: { sb.append("<br/>"); break; } case Span.QTI_CLASS_NAME: { renderSpan(renderer, sb, (Span)inline, component, resolvedAssessmentItem, itemSessionState, ubu, translator); break; } case A.QTI_CLASS_NAME: { renderA(renderer, sb, (A)inline, component, resolvedAssessmentItem, itemSessionState, ubu, translator); break; } case Object.QTI_CLASS_NAME: { renderObject(sb, (Object)inline, component, resolvedAssessmentItem); break; } default: { renderStartHtmlTag(sb, component, resolvedAssessmentItem, inline, null); if(inline instanceof SimpleInline) { SimpleInline simpleInline = (SimpleInline)inline; simpleInline.getInlines().forEach((child) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); } renderEndTag(sb, inline); } } } protected final void renderSpan(AssessmentRenderer renderer, StringOutput sb, Span span, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, URLBuilder ubu, Translator translator) { Attribute<?> attrClass = span.getAttributes().get("class"); if(attrClass != null && attrClass.getValue() != null && attrClass.getValue().toString().equals("[math]")) { String domid = "mw_" + CodeHelper.getRAMUniqueID(); sb.append("<span id=\"").append(domid).append("\">"); renderStartHtmlTag(sb, component, resolvedAssessmentItem, span, null); span.getInlines().forEach((child) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); renderEndTag(sb, span); sb.append("</span>") .append("\n<script type='text/javascript'>\n/* <![CDATA[ */\n jQuery(function() {setTimeout(function() { BFormatter.formatLatexFormulas('").append(domid).append("');}, 100); }); \n/* ]]> */\n</script>"); } else { renderStartHtmlTag(sb, component, resolvedAssessmentItem, span, null); span.getInlines().forEach((child) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); renderEndTag(sb, span); } } protected final void renderA(AssessmentRenderer renderer, StringOutput sb, A a, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, URLBuilder ubu, Translator translator) { sb.append("<a"); boolean target = false; for(Attribute<?> attribute:a.getAttributes()) { String value = getHtmlAttributeValue(component, resolvedAssessmentItem, attribute); if(StringHelper.containsNonWhitespace(value)) { String name = attribute.getLocalName(); if("target".equals(name)) { target = true; } sb.append(" ").append(name).append("=\"").append(value).append("\""); } } if(!target) { sb.append(" target=\"_blank\""); } sb.append(">"); a.getInlines().forEach((child) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, child, ubu, translator)); renderEndTag(sb, a); } protected final void renderObject(StringOutput sb, Object object, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem) { Attribute<?> attrId = object.getAttributes().get("id"); if(attrId != null && attrId.getValue() != null && attrId.getValue().toString().startsWith("olatFlashMovieViewer")) { //this is a OpenOLAT movie and need to be converted /* <span id="olatFlashMovieViewer213060" class="olatFlashMovieViewer" style="display:block;border:solid 1px #000; width:320px; height:240px;"> <script src="/raw/fx-111111x11/movie/player.js" type="text/javascript"></script> <script type="text/javascript" defer="defer">// <![CDATA[ BPlayer.insertPlayer("demo-video.mp4","olatFlashMovieViewer213060",320,240,0,0,"video",undefined,false,false,true,undefined); // ]]></script> </span> */ String id = attrId.getValue().toString(); Attribute<?> dataAttr = object.getAttributes().get("data"); String data = dataAttr.getValue().toString(); Attribute<?> attrDataMovie = object.getAttributes().get("data-oo-movie"); String dataMovie = attrDataMovie.getValue().toString(); if(data != null && !data.startsWith("http://") && !data.startsWith("https://")) { String relativePath = component.relativePathTo(resolvedAssessmentItem); String src = Settings.createServerURI() + component.getMapperUri() + relativePath + "/" + data; dataMovie = dataMovie.replace(data, src); } String height = "240"; String width = "320"; //try to guess the height and width if(dataMovie != null) { String[] dataMovieParts = dataMovie.split(","); if(dataMovieParts.length > 3) { width = dataMovieParts[2]; height = dataMovieParts[3]; } } sb.append("<span id=\"").append(id).append("\" class=\"olatFlashMovieViewer\" style=\"display:block;border:solid 1px #000; width:").append(width).append("px; height:").append(height).append("px;\">\n") .append(" <script src=\""); Renderer.renderStaticURI(sb, "movie/player.js"); sb.append("\" type=\"text/javascript\"></script>\n") .append(" <script type=\"text/javascript\" defer=\"defer\">// <![CDATA[\n") .append(" BPlayer.insertPlayer(").append(dataMovie).append(");\n") .append(" // ]]></script>\n") .append("</span>\n"); } else { renderStartHtmlTag(sb, component, resolvedAssessmentItem, object, null); //TODO object.getObjectFlows(); renderEndTag(sb, object); } } protected final void renderInfoControl(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, InfoControl infoControl, URLBuilder ubu, Translator translator) { sb.append("<div class=\"infoControl\">") .append("<button type='button' onclick=\"return QtiWorksRendering.showInfoControlContent(this)\" class='btn btn-default'>") .append("<span>").append(StringHelper.escapeHtml(infoControl.getTitle())).append("</span></button>") .append("<div class='infoControlContent o_info'>"); infoControl.getChildren().forEach((flow) -> renderFlow(renderer, sb, component, resolvedAssessmentItem, itemSessionState, flow, ubu, translator)); sb.append("</div></div>"); } protected final void renderHtmlTag(StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, QtiNode node, String cssClass) { sb.append("<").append(node.getQtiClassName()); for(Attribute<?> attribute:node.getAttributes()) { String value = getHtmlAttributeValue(component, resolvedAssessmentItem, attribute); if(StringHelper.containsNonWhitespace(value)) { String name = attribute.getLocalName(); sb.append(" ").append(name).append("=\"").append(value); if(cssClass != null && name.equals("class")) { sb.append(" ").append(cssClass); } sb.append("\""); } } sb.append(" />"); } protected final void renderStartHtmlTag(StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, QtiNode node, String cssClass) { sb.append("<").append(node.getQtiClassName()); renderHtmlTagAttributes(sb, component, resolvedAssessmentItem, node, cssClass); sb.append(">"); } protected final void renderHtmlTagAttributes(StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, QtiNode node, String cssClass) { for(Attribute<?> attribute:node.getAttributes()) { String value = getHtmlAttributeValue(component, resolvedAssessmentItem, attribute); if(StringHelper.containsNonWhitespace(value)) { String name = attribute.getLocalName(); sb.append(" ").append(name).append("=\"").append(value); if(cssClass != null && name.equals("class")) { sb.append(" ").append(cssClass); } sb.append("\""); } } } protected void renderEndTag(StringOutput sb, QtiNode node) { sb.append("</").append(node.getQtiClassName()).append(">"); } /* <xsl:choose> <xsl:when test="$allowComment and $isItemSessionOpen"> <fieldset class="candidateComment"> <legend>Please use the following text box if you need to provide any additional information, comments or feedback during this test:</legend> <input name="qtiworks_comment_presented" type="hidden" value="true"/> <textarea name="qtiworks_comment"><xsl:value-of select="$itemSessionState/qw:candidateComment"/></textarea> </fieldset> </xsl:when> <xsl:when test="$allowComment and $isItemSessionEnded and exists($itemSessionState/qw:candidateComment)"> <fieldset class="candidateComment"> <legend>You submitted the folllowing comment with this item:</legend> <input name="qtiworks_comment_presented" type="hidden" value="true"/> <textarea name="qtiworks_comments" disabled="disabled"><xsl:value-of select="$itemSessionState/qw:candidateComment"/></textarea> </fieldset> </xsl:when> </xsl:choose> */ protected void renderComment(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ItemSessionState itemSessionState, Translator translator) { if(renderer.isCandidateCommentAllowed()) { if(component.isItemSessionOpen(itemSessionState, renderer.isSolutionMode())) { String comment = itemSessionState.getCandidateComment(); renderComment(sb, comment, false, translator); } else if(component.isItemSessionEnded(itemSessionState, renderer.isSolutionMode()) && StringHelper.containsNonWhitespace(itemSessionState.getCandidateComment())) { String comment = itemSessionState.getCandidateComment(); renderComment(sb, comment, true, translator); } } } private void renderComment(StringOutput sb, String comment, boolean disabled, Translator translator) { sb.append("<fieldset class='o_candidatecomment'>") .append("<legend>").append(translator.translate("assessment.comment.legend")).append("</legend>") .append("<input name='qtiworks_comment_presented' type='hidden' value='true' />") .append("<textarea name='qtiworks_comment'").append(" disabled=\"disabled\"", disabled).append(" rows='4' class='form-control'>"); if(StringHelper.containsNonWhitespace(comment)) { sb.append(comment); } sb.append("</textarea></fieldset>"); } private void renderEndAttemptInteraction(AssessmentRenderer renderer, StringOutput sb, EndAttemptInteraction interaction, ItemSessionState itemSessionState, AssessmentObjectComponent component, URLBuilder ubu, Translator translator) { boolean ended = component.isItemSessionEnded(itemSessionState, renderer.isSolutionMode()); AssessmentObjectFormItem item = component.getQtiItem(); String responseUniqueId = component.getResponseUniqueIdentifier(itemSessionState, interaction); String id = "qtiworks_response_".concat(responseUniqueId); if(!ended) { sb.append("<input name=\"qtiworks_presented_").append(responseUniqueId).append("\" type=\"hidden\" value=\"1\"/>"); } FormItem endAttemptButton = item.getFormComponent(id); if(endAttemptButton == null) { String title = StringHelper.escapeHtml(interaction.getTitle()); FormLink button = FormUIFactory.getInstance().addFormLink(id, id, title, null, null, Link.BUTTON | Link.NONTRANSLATED); // use specific icon for known types if (interaction.getResponseIdentifier().equals(QTI21Constants.HINT_REQUEST_IDENTIFIER)) { button.setIconLeftCSS("o_icon o_icon-fw o_icon_qti_hint"); button.setElementCssClass("o_sel_assessment_item_hint"); } endAttemptButton = button; endAttemptButton.setTranslator(translator); endAttemptButton.setUserObject(interaction); if(item.getRootForm() != endAttemptButton.getRootForm()) { endAttemptButton.setRootForm(item.getRootForm()); } item.addFormItem(endAttemptButton); } endAttemptButton.setEnabled(!ended); endAttemptButton.getComponent().getHTMLRendererSingleton() .render(renderer.getRenderer(), sb, endAttemptButton.getComponent(), ubu, translator, new RenderResult(), null); } private void renderPositionObjectStage(AssessmentRenderer renderer, StringOutput sb, PositionObjectStage positionObjectStage, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, AssessmentObjectComponent component, URLBuilder ubu, Translator translator) { Context ctx = new VelocityContext(); ctx.put("positionObjectStage", positionObjectStage); String page = getInteractionTemplate(positionObjectStage); renderVelocity(renderer, sb, positionObjectStage, ctx, page, resolvedAssessmentItem, itemSessionState, component, ubu, translator); } /** * Render the interaction or the PositionStageObject * @param renderer * @param sb * @param interaction * @param assessmentItem * @param itemSessionState * @param component * @param ubu * @param translator */ private void renderCustomInteraction(AssessmentRenderer renderer, StringOutput sb, CustomInteraction<?> interaction, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, AssessmentObjectComponent component, URLBuilder ubu, Translator translator) { Context ctx = new VelocityContext(); ctx.put("interaction", interaction); String page; if(interaction instanceof MathEntryInteraction) { page = velocity_root.concat("/mathEntryInteraction.html"); } else { page = velocity_root.concat("/unsupportedCustomInteraction.html"); } renderVelocity(renderer, sb, interaction, ctx, page, resolvedAssessmentItem, itemSessionState, component, ubu, translator); } /** * Render the interaction or the PositionStageObject * @param renderer * @param sb * @param interaction * @param assessmentItem * @param itemSessionState * @param component * @param ubu * @param translator */ private void renderInteraction(AssessmentRenderer renderer, StringOutput sb, FlowInteraction interaction, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, AssessmentObjectComponent component, URLBuilder ubu, Translator translator) { Context ctx = new VelocityContext(); ctx.put("interaction", interaction); String page = getInteractionTemplate(interaction); renderVelocity(renderer, sb, interaction, ctx, page, resolvedAssessmentItem, itemSessionState, component, ubu, translator); } private void renderVelocity(AssessmentRenderer renderer, StringOutput sb, QtiNode interaction, Context ctx, String page, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, AssessmentObjectComponent component, URLBuilder ubu, Translator translator) { ctx.put("localName", interaction.getQtiClassName()); ctx.put("assessmentItem", resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful()); ctx.put("itemSessionState", itemSessionState); ctx.put("isItemSessionOpen", component.isItemSessionOpen(itemSessionState, renderer.isSolutionMode())); ctx.put("isItemSessionEnded", component.isItemSessionEnded(itemSessionState, renderer.isSolutionMode())); Renderer fr = Renderer.getInstance(component, translator, ubu, new RenderResult(), renderer.getGlobalSettings()); AssessmentRenderer fHints = renderer.newHints(fr); AssessmentObjectVelocityRenderDecorator vrdec = new AssessmentObjectVelocityRenderDecorator(fHints, sb, component, resolvedAssessmentItem, itemSessionState, ubu, translator); ctx.put("r", vrdec); VelocityHelper vh = VelocityHelper.getInstance(); vh.mergeContent(page, ctx, sb, null); ctx.remove("r"); IOUtils.closeQuietly(vrdec); } private String getInteractionTemplate(QtiNode interaction) { String interactionName; switch(interaction.getQtiClassName()) { case "matchInteraction": { MatchInteraction matchInteraction = (MatchInteraction)interaction; interactionName = interaction.getQtiClassName(); if(matchInteraction.getResponseIdentifier().toString().startsWith("KPRIM_")) { interactionName += "_kprim"; } break; } case "mathEntryInteraction": { if(!CoreSpringFactory.getImpl(QTI21Module.class).isMathAssessExtensionEnabled()) { interactionName = "mathEntryInteractionNotEnabled"; break; } } default: interactionName = interaction.getQtiClassName(); break; } String templateName = interactionName.substring(0, 1).toLowerCase().concat(interactionName.substring(1)); return velocity_root + "/" + templateName + ".html"; } /* <xsl:template match="qti:gap"> <xsl:variable name="gmi" select="ancestor::qti:gapMatchInteraction" as="element(qti:gapMatchInteraction)"/> <xsl:variable name="gaps" select="$gmi//qti:gap" as="element(qti:gap)+"/> <xsl:variable name="thisGap" select="." as="element(qti:gap)"/> <span class="gap" id="qtiworks_id_{$gmi/@responseIdentifier}_{@identifier}"> <!-- (Print index of this gap wrt all gaps in the interaction) --> GAP <xsl:value-of select="for $i in 1 to count($gaps) return if ($gaps[$i]/@identifier = $thisGap/@identifier) then $i else ()"/> </span> </xsl:template> */ private void renderGap(StringOutput sb, Gap gap, ItemSessionState itemSessionState, AssessmentObjectComponent component) { GapMatchInteraction interaction = null; for(QtiNode parentNode=gap.getParent(); parentNode.getParent() != null; parentNode = parentNode.getParent()) { if(parentNode instanceof GapMatchInteraction) { interaction = (GapMatchInteraction)parentNode; break; } } if(interaction != null) { List<Gap> gaps = QueryUtils.search(Gap.class, interaction.getBlockStatics()); String responseUniqueId = component.getResponseUniqueIdentifier(itemSessionState, interaction); sb.append("<span class='gap' id=\"qtiworks_id_").append(responseUniqueId) .append("_").append(gap.getIdentifier().toString()).append("\">"); sb.append("GAP ").append(gaps.indexOf(gap)); sb.append("</span>"); } } /* <xsl:template match="qti:hottext"> <xsl:if test="qw:is-visible(.)"> <xsl:variable name="hottextInteraction" select="ancestor::qti:hottextInteraction" as="element(qti:hottextInteraction)"/> <xsl:variable name="responseIdentifier" select="$hottextInteraction/@responseIdentifier" as="xs:string"/> <span class="hottext"> <input type="{if ($hottextInteraction/@maxChoices=1) then 'radio' else 'checkbox'}" name="qtiworks_response_{$responseIdentifier}" value="{@identifier}"> <xsl:if test="$isItemSessionEnded"> <xsl:attribute name="disabled">disabled</xsl:attribute> </xsl:if> <xsl:if test="qw:value-contains(qw:get-response-value(/, $responseIdentifier), @identifier)"> <xsl:attribute name="checked" select="'checked'"/> </xsl:if> </input> <xsl:apply-templates/> </span> </xsl:if> </xsl:template> */ private void renderHottext(AssessmentRenderer renderer, StringOutput sb, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Hottext hottext, AssessmentObjectComponent component, URLBuilder ubu, Translator translator) { if(!isVisible(hottext, itemSessionState)) return; HottextInteraction interaction = null; for(QtiNode parentNode=hottext.getParent(); parentNode.getParent() != null; parentNode = parentNode.getParent()) { if(parentNode instanceof HottextInteraction) { interaction = (HottextInteraction)parentNode; break; } } if(interaction != null) { sb.append("<span class='hottext'><input type='"); if(interaction.getMaxChoices() == 1) { sb.append("radio"); } else { sb.append("checkbox"); } String guid = "oo_" + CodeHelper.getRAMUniqueID(); String responseUniqueId = component.getResponseUniqueIdentifier(itemSessionState, interaction); sb.append("' id='").append(guid).append("' name='qtiworks_response_").append(responseUniqueId).append("'") .append(" value='").append(hottext.getIdentifier().toString()).append("'"); if(component.isItemSessionEnded(itemSessionState, renderer.isSolutionMode())) { sb.append(" disabled"); } AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); Value responseValue = getResponseValue(assessmentItem, itemSessionState, interaction.getResponseIdentifier(), renderer.isSolutionMode()); if(valueContains(responseValue, hottext.getIdentifier())) { sb.append(" checked"); } sb.append(" />"); sb.append("<label for='").append(guid).append("'>"); hottext.getInlineStatics().forEach((inline) -> renderInline(renderer, sb, component, resolvedAssessmentItem, itemSessionState, inline, ubu, translator)); FormJSHelper.appendFlexiFormDirtyOn(sb, component.getQtiItem().getRootForm(), "change click", guid); sb.append("</label></span>"); } } protected void renderExtendedTextBox(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, AssessmentItem assessmentItem, ItemSessionState itemSessionState, ExtendedTextInteraction interaction) { ResponseData responseInput = getResponseInput(itemSessionState, interaction.getResponseIdentifier()); ResponseDeclaration responseDeclaration = getResponseDeclaration(assessmentItem, interaction.getResponseIdentifier()); Cardinality cardinality = responseDeclaration == null ? null : responseDeclaration.getCardinality(); if(cardinality != null && (cardinality.isRecord() || cardinality.isSingle())) { String responseInputString = extractSingleCardinalityResponseInput(responseInput); renderExtendedTextBox(renderer, sb, component, assessmentItem, itemSessionState, interaction, responseInputString); } else { if(interaction.getMaxStrings() != null) { int maxStrings = interaction.getMaxStrings().intValue(); for(int i=0; i<maxStrings; i++) { String responseInputString = extractResponseInputAt(responseInput, i); renderExtendedTextBox(renderer, sb, component, assessmentItem, itemSessionState, interaction, responseInputString); } } else { // <xsl:with-param name="stringsCount" select="if (exists($responseValue)) then max(($minStrings, qw:get-cardinality-size($responseValue))) else $minStrings"/> int stringCounts = interaction.getMinStrings(); Value responseValue = AssessmentRenderFunctions .getResponseValue(assessmentItem, itemSessionState, interaction.getResponseIdentifier(), renderer.isSolutionMode()); if(exists(responseValue)) { stringCounts = java.lang.Math.max(interaction.getMinStrings(), getCardinalitySize(responseValue)); } for(int i=0; i<stringCounts; i++) { String responseInputString = extractResponseInputAt(responseInput, i); renderExtendedTextBox(renderer, sb, component, assessmentItem, itemSessionState, interaction, responseInputString); } } } } /* <xsl:template match="qti:extendedTextInteraction" mode="multibox"> <xsl:param name="responseInput" as="element(qw:responseInput)?"/> <xsl:param name="checkJavaScript" as="xs:string?"/> <xsl:param name="stringsCount" as="xs:integer"/> <xsl:param name="allowCreate" select="false()" as="xs:boolean"/> <xsl:variable name="interaction" select="." as="element(qti:extendedTextInteraction)"/> <xsl:for-each select="1 to $stringsCount"> <xsl:variable name="i" select="." as="xs:integer"/> <xsl:apply-templates select="$interaction" mode="singlebox"> <xsl:with-param name="responseInputString" select="$responseInput/qw:string[position()=$i]"/> <xsl:with-param name="checkJavaScript" select="$checkJavaScript"/> <xsl:with-param name="allowCreate" select="$allowCreate and $i=$stringsCount"/> </xsl:apply-templates> <br /> </xsl:for-each> </xsl:template> <xsl:template match="qti:extendedTextInteraction" mode="singlebox"> <xsl:param name="responseInputString" as="xs:string?"/> <xsl:param name="checkJavaScript" as="xs:string?"/> <xsl:param name="allowCreate" select="false()" as="xs:boolean"/> <xsl:variable name="is-bad-response" select="qw:is-bad-response(@responseIdentifier)" as="xs:boolean"/> <xsl:variable name="is-invalid-response" select="qw:is-invalid-response(@responseIdentifier)" as="xs:boolean"/> <textarea cols="72" rows="6" name="qtiworks_response_{@responseIdentifier}"> <xsl:if test="$isItemSessionEnded"> <xsl:attribute name="disabled">disabled</xsl:attribute> </xsl:if> <xsl:if test="$is-bad-response or $is-invalid-response"> <xsl:attribute name="class" select="'badResponse'"/> </xsl:if> <xsl:if test="@expectedLines"> <xsl:attribute name="rows" select="@expectedLines"/> </xsl:if> <xsl:if test="@expectedLines and @expectedLength"> <xsl:attribute name="cols" select="ceiling(@expectedLength div @expectedLines)"/> </xsl:if> <xsl:if test="$checkJavaScript"> <xsl:attribute name="onchange" select="$checkJavaScript"/> </xsl:if> <xsl:if test="$allowCreate"> <xsl:attribute name="onkeyup" select="'QtiWorksRendering.addNewTextBox(this)'"/> </xsl:if> <xsl:value-of select="$responseInputString"/> </textarea> </xsl:template> */ protected void renderExtendedTextBox(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, AssessmentItem assessmentItem, ItemSessionState itemSessionState, ExtendedTextInteraction interaction, String responseInputString) { String responseUniqueId = component.getResponseUniqueIdentifier(itemSessionState, interaction); sb.append("<textarea id='oo_").append(responseUniqueId).append("' name='qtiworks_response_").append(responseUniqueId).append("'"); boolean ended = component.isItemSessionEnded(itemSessionState, renderer.isSolutionMode()); if(ended) { sb.append(" disabled"); } if(StringHelper.containsNonWhitespace(interaction.getPlaceholderText())) { sb.append(" placeholder=\"").append(StringHelper.escapeHtml(interaction.getPlaceholderText())).append("\""); } if(isBadResponse(itemSessionState, interaction.getResponseIdentifier()) || isInvalidResponse(itemSessionState, interaction.getResponseIdentifier())) { sb.append(" class='form-control badResponse'"); } else { sb.append(" class='form-control'"); } int expectedLines = interaction.getExpectedLines() == null ? 6 : interaction.getExpectedLines().intValue(); sb.append(" rows='").append(expectedLines).append("'"); if(interaction.getExpectedLength() == null) { sb.append(" cols='72'"); } else { int cols = interaction.getExpectedLength().intValue() / expectedLines; sb.append(" cols='").append(cols).append("'"); } ResponseDeclaration responseDeclaration = getResponseDeclaration(assessmentItem, interaction.getResponseIdentifier()); String checkJavascript = checkJavaScript(responseDeclaration, interaction.getPatternMask()); if(StringHelper.containsNonWhitespace(checkJavascript)) { sb.append(" onchange=\"").append(checkJavascript).append("\""); } sb.append(">"); if(renderer.isSolutionMode()) { String placeholder = interaction.getPlaceholderText(); if(StringHelper.containsNonWhitespace(placeholder)) { sb.append(placeholder); } } else if( StringHelper.containsNonWhitespace(responseInputString)) { sb.append(responseInputString); } sb.append("</textarea>"); if(!ended) { FormJSHelper.appendFlexiFormDirty(sb, component.getQtiItem().getRootForm(), "oo_" + responseUniqueId); sb.append(FormJSHelper.getJSStartWithVarDeclaration("oo_" + responseUniqueId)) //plain textAreas should not propagate the keypress "enter" (keynum = 13) as this would submit the form .append("oo_").append(responseUniqueId).append(".on('keypress', function(event, target){if (13 == event.keyCode) {event.stopPropagation()} })") .append(FormJSHelper.getJSEnd()); Form form = component.getQtiItem().getRootForm(); sb.append(FormJSHelper.getJSStart()) .append("jQuery(function() {\n") .append(" jQuery('#").append("oo_").append(responseUniqueId).append("').qtiAutosave({\n") .append(" responseUniqueId:'").append(responseUniqueId).append("',\n") .append(" formName:'").append(form.getFormName()).append("',\n") .append(" dispIdField:'").append(form.getDispatchFieldId()).append("',\n") .append(" dispId:'").append(component.getQtiItem().getFormDispatchId()).append("',\n") .append(" eventIdField:'").append(form.getEventFieldId()).append("'\n") .append(" });\n") .append("})\n") .append(FormJSHelper.getJSEnd()); } } protected abstract void renderPrintedVariable(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, PrintedVariable printedVar); /** * The QTI spec says that this variable must have single cardinality. * * For convenience, we also accept multiple, ordered and record cardinality variables here, * printing them out in a hard-coded form that probably won't make sense to test * candidates but might be useful for debugging. * * Our implementation additionally adds support for "printing" MathsContent variables * used in MathAssess, outputting an inline Presentation MathML element, as documented * in the MathAssses spec. */ protected void renderPrintedVariable(AssessmentRenderer renderer, StringOutput sb, PrintedVariable source, VariableDeclaration valueDeclaration, Value valueHolder) { if(isNullValue(valueHolder)) { //(Spec says to output nothing in this case) } else if(isSingleCardinalityValue(valueHolder)) { if(valueDeclaration.hasBaseType(BaseType.INTEGER) || valueDeclaration.hasBaseType(BaseType.FLOAT)) { renderSingleCardinalityValue(sb, valueHolder); } else { renderSingleCardinalityValue(sb, valueHolder); } // math content is a record with special markers } else if (isMathsContentValue(valueHolder)) { //<xsl:copy-of select="qw:extract-maths-content-pmathml($valueHolder)"/> String mathMlContent = extractMathsContentPmathml(valueHolder); if(renderer.isMathXsltDisabled()) { sb.append(mathMlContent); } else { transformMathmlAsString(sb, mathMlContent); } } else if(isMultipleCardinalityValue(valueHolder)) { String delimiter = source.getDelimiter(); if(!StringHelper.containsNonWhitespace(delimiter)) { delimiter = ";"; } renderMultipleCardinalityValue(sb, valueHolder, delimiter); } else if(isOrderedCardinalityValue(valueHolder)) { if(source.getIndex() != null) { int index = -1; if(source.getIndex().isConstantInteger()) { index = source.getIndex().getConstantIntegerValue().intValue(); } else if(source.getIndex().isVariableRef()) { //TODO qti what to do??? } SingleValue indexedValue = extractIterableElement(valueHolder, index); renderSingleCardinalityValue(sb, indexedValue); } else { String delimiter = source.getDelimiter(); if(!StringHelper.containsNonWhitespace(delimiter)) { delimiter = ";"; } renderOrderedCardinalityValue(sb, valueHolder, delimiter); } } else if(isRecordCardinalityValue(valueHolder)) { String field = source.getField(); if(StringHelper.containsNonWhitespace(field)) { Identifier fieldIdentifier = Identifier.assumedLegal(field); SingleValue mappedValue = extractRecordFieldValue(valueHolder, fieldIdentifier); renderSingleCardinalityValue(sb, mappedValue); } else { String delimiter = source.getDelimiter(); String mappingIndicator = source.getMappingIndicator(); if(!StringHelper.containsNonWhitespace(delimiter)) { delimiter = ";"; } if(!StringHelper.containsNonWhitespace(mappingIndicator)) { mappingIndicator = "="; } renderRecordCardinalityValue(sb, valueHolder, delimiter, mappingIndicator); } } else { sb.append("printedVariable may not be applied to value ").append(valueHolder.toString()); } } protected void renderMath(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, Math math) { renderer.setMathXsltDisabled(true); StringOutput mathOutput = StringOutputPool.allocStringBuilder(2048); mathOutput.append("<math xmlns=\"http://www.w3.org/1998/Math/MathML\">"); math.getContent().forEach((foreignElement) -> renderMath(renderer, mathOutput, component, resolvedAssessmentItem, itemSessionState, foreignElement)); mathOutput.append("</math>"); String enrichedMathML = StringOutputPool.freePop(mathOutput); renderer.setMathXsltDisabled(false); transformMathmlAsString(sb, enrichedMathML); } protected void renderMath(AssessmentRenderer renderer, StringOutput out, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, QtiNode mathElement) { if(mathElement instanceof ForeignElement) { ForeignElement fElement = (ForeignElement)mathElement; boolean mi = fElement.getQtiClassName().equals("mi"); boolean ci = fElement.getQtiClassName().equals("ci"); if(ci || mi) { AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); String text = contentAsString(fElement); Identifier identifier = Identifier.assumedLegal(text); Value templateValue = getTemplateValue(itemSessionState, text); Value outcomeValue = getOutcomeValue(itemSessionState, text); Value responseValue = getResponseValue(assessmentItem, itemSessionState, identifier, renderer.isSolutionMode()); if(templateValue != null && isTemplateDeclarationAMathVariable(assessmentItem, text)) { if(ci) { substituteCi(out, templateValue); } else if(mi) { substituteMi(out, templateValue); } } else if(outcomeValue != null) { if(ci) { substituteCi(out, outcomeValue); } else if(mi) { substituteMi(out, outcomeValue); } } else if(responseValue != null) { if(ci) { substituteCi(out, responseValue); } else if(mi) { substituteMi(out, responseValue); } } else { renderStartHtmlTag(out, component, resolvedAssessmentItem, fElement, null); fElement.getChildren().forEach((child) -> renderMath(renderer, out, component, resolvedAssessmentItem, itemSessionState, child)); renderEndTag(out, fElement); } } else { renderStartHtmlTag(out, component, resolvedAssessmentItem, fElement, null); fElement.getChildren().forEach((child) -> renderMath(renderer, out, component, resolvedAssessmentItem, itemSessionState, child)); renderEndTag(out, fElement); } } else if(mathElement instanceof TextRun) { out.append(((TextRun)mathElement).getTextContent()); } } /* <xsl:template name="substitute-mi" as="element()"> <xsl:param name="identifier" as="xs:string"/> <xsl:param name="value" as="element()"/> <xsl:choose> <xsl:when test="qw:is-null-value($value)"> <!-- We shall represent null as an empty mrow --> <xsl:element name="mrow" namespace="http://www.w3.org/1998/Math/MathML"/> </xsl:when> <xsl:when test="qw:is-single-cardinality-value($value)"> <!-- Single cardinality template variables are substituted according to Section 6.3.1 of the spec. Note that it does not define what should be done with multiple and ordered cardinality variables. --> <xsl:element name="mn" namespace="http://www.w3.org/1998/Math/MathML"> <xsl:copy-of select="@*"/> <xsl:value-of select="qw:extract-single-cardinality-value($value)"/> </xsl:element> </xsl:when> <xsl:when test="qw:is-maths-content-value($value)"> <!-- This is a MathAssess MathsContent variable. What we do here is replace the matched MathML element with the child(ren) of the <math/> PMathML field in this record, wrapping in an <mrow/> if required so as to ensure that we have a single replacement element --> <xsl:variable name="pmathml" select="qw:extract-maths-content-pmathml($value)" as="element(m:math)"/> <xsl:choose> <xsl:when test="count($pmathml/*)=1"> <xsl:copy-of select="$pmathml/*"/> </xsl:when> <xsl:otherwise> <xsl:element name="mrow" namespace="http://www.w3.org/1998/Math/MathML"> <xsl:copy-of select="$pmathml/*"/> </xsl:element> </xsl:otherwise> </xsl:choose> </xsl:when> <xsl:otherwise> <!-- Unsupported substitution --> <xsl:message> Substituting the variable <xsl:value-of select="$identifier"/> with value <xsl:copy-of select="$value"/> within MathML is not currently supported. </xsl:message> <xsl:element name="mtext" namespace="http://www.w3.org/1998/Math/MathML">(Unsupported variable substitution)</xsl:element> </xsl:otherwise> </xsl:choose> </xsl:template> */ protected void substituteMi(StringOutput sb, Value value) { if(value == null || value.isNull()) { sb.append("<mrow />"); } else if(isSingleCardinalityValue(value)) { sb.append("<mn>");//<xsl:copy-of select="@*"/> renderSingleCardinalityValue(sb, value); sb.append("</mn>"); } else if(isMathsContentValue(value)) { String mathMlContent = extractMathsContentPmathml(value); sb.append(mathMlContent); } else { //not supported } } /* <xsl:template name="substitute-ci" as="element()*"> <xsl:param name="identifier" as="xs:string"/> <xsl:param name="value" as="element()"/> <xsl:choose> <xsl:when test="qw:is-null-value($value)"> <!-- We shall omit nulls --> </xsl:when> <xsl:when test="qw:is-single-cardinality-value($value)"> <!-- Single cardinality template variables are substituted according to Section 6.3.1 of the spec. Note that it does not define what should be done with multiple and ordered cardinality variables. --> <xsl:element name="cn" namespace="http://www.w3.org/1998/Math/MathML"> <xsl:copy-of select="@*"/> <xsl:value-of select="qw:extract-single-cardinality-value($value)"/> </xsl:element> </xsl:when> <xsl:when test="qw:is-maths-content-value($value)"> <!-- This is a MathAssess MathsContent variable. What we do here is replace the matched MathML element with the child(ren) of the <math/> PMathML field in this record, wrapping in an <mrow/> if required so as to ensure that we have a single replacement element --> <xsl:variable name="cmathml" select="qw:extract-maths-content-cmathml($value)" as="element(m:math)"/> <xsl:copy-of select="$cmathml/*"/> </xsl:when> <xsl:otherwise> <!-- Unsupported substitution --> <xsl:message> Substituting the variable <xsl:value-of select="$identifier"/> with value <xsl:copy-of select="$value"/> within MathML is not currently supported. </xsl:message> <xsl:element name="mtext" namespace="http://www.w3.org/1998/Math/MathML">(Unsupported variable substitution)</xsl:element> </xsl:otherwise> </xsl:choose> </xsl:template> */ protected void substituteCi(StringOutput sb, Value value) { if(value == null || value.isNull()) { //we omit null } else if(isSingleCardinalityValue(value)) { sb.append("<cn>");//<xsl:copy-of select="@*"/> renderSingleCardinalityValue(sb, value); sb.append("</cn>"); } else if(isMathsContentValue(value)) { String mathMlContent = extractMathsContentPmathml(value); sb.append(mathMlContent); } else { //not supported } } protected void transformMathmlAsString(StringOutput sb, String mathmlAsString) { if(!StringHelper.containsNonWhitespace(mathmlAsString)) { return; } XsltStylesheetManager stylesheetManager = CoreSpringFactory.getImpl(QTI21Service.class).getXsltStylesheetManager(); final TransformerHandler mathmlTransformerHandler = stylesheetManager.getCompiledStylesheetHandler(ctopXsltUri, null); try { mathmlTransformerHandler.setResult(new StreamResult(sb)); final XMLReader xmlReader = XmlUtilities.createNsAwareSaxReader(); xmlReader.setContentHandler(mathmlTransformerHandler); Reader mathStream = new StringReader(mathmlAsString); InputSource assessmentSaxSource = new InputSource(mathStream); xmlReader.parse(assessmentSaxSource); } catch (final Exception e) { log.error("Rendering XSLT pipeline failed for request {}", e); throw new OLATRuntimeException("Unexpected Exception running rendering XML pipeline", e); } } protected boolean containsClass(QtiNode element, String marker) { AttributeList attributes = element.getAttributes(); for(int i=attributes.size(); i-->0; ) { Attribute<?> attr = attributes.get(i); if("class".equals(attr.getLocalName())) { if(attr instanceof ForeignAttribute) { String css = ((ForeignAttribute)attr).getValue(); return css != null && css.contains(marker); } else if(attr instanceof StringMultipleAttribute) { List<String> css = ((StringMultipleAttribute)attr).getValue(); return css != null && css.contains(marker); } } } return false; } public static class RenderingRequest { private boolean reviewMode; private boolean solutionMode; private boolean testPartNavigationAllowed; private boolean advanceTestItemAllowed; private boolean nextItemAllowed; private boolean endTestPartAllowed; private RenderingRequest(boolean reviewMode, boolean solutionMode, boolean testPartNavigationAllowed, boolean advanceTestItemAllowed, boolean nextItemAllowed, boolean endTestPartAllowed) { this.reviewMode = reviewMode; this.solutionMode = solutionMode; this.testPartNavigationAllowed = testPartNavigationAllowed; this.advanceTestItemAllowed = advanceTestItemAllowed; this.nextItemAllowed = nextItemAllowed; this.endTestPartAllowed = endTestPartAllowed; } public boolean isReviewMode() { return reviewMode; } public boolean isSolutionMode() { return solutionMode; } public boolean isTestPartNavigationAllowed() { return testPartNavigationAllowed; } public boolean isAdvanceTestItemAllowed() { return advanceTestItemAllowed; } public boolean isNextItemAllowed() { return nextItemAllowed; } public boolean isEndTestPartAllowed() { return endTestPartAllowed; } public static RenderingRequest getItemSolution() { return new RenderingRequest(true, true, false, false, false, false); } public static RenderingRequest getItemReview() { return new RenderingRequest(true, false, false, false, false, false); } public static RenderingRequest getItem(TestSessionController testSessionController) { final TestPart currentTestPart = testSessionController.getCurrentTestPart(); final NavigationMode navigationMode = currentTestPart == null ? null : currentTestPart.getNavigationMode(); boolean nextItemAllowed = navigationMode == NavigationMode.NONLINEAR; boolean advanceTestItemAllowed = navigationMode == NavigationMode.LINEAR && testSessionController.getTestSessionState().getCurrentItemKey() != null//mayAdvanceItemLinear assert on the current selected item && testSessionController.mayAdvanceItemLinear(); boolean testPartNavigationAllowed = navigationMode == NavigationMode.NONLINEAR; boolean endTestPartAllowed = navigationMode == NavigationMode.LINEAR && testSessionController.getCurrentTestPart() != null && testSessionController.mayEndCurrentTestPart(); return new RenderingRequest(false, false, testPartNavigationAllowed, advanceTestItemAllowed, nextItemAllowed, endTestPartAllowed); } } }