/** * <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.contentAsString; import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.testFeedbackVisible; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.lang.StringEscapeUtils; import org.olat.core.gui.components.Component; 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.form.flexible.impl.NameValuePair; 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.URLBuilder; import org.olat.core.gui.translator.Translator; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.ims.qti21.AssessmentTestSession; import org.olat.ims.qti21.model.audit.CandidateEvent; import org.olat.ims.qti21.model.audit.CandidateTestEventType; import org.olat.ims.qti21.ui.CandidateSessionContext; import org.olat.ims.qti21.ui.QTIWorksAssessmentTestEvent.Event; import org.w3c.dom.Element; import uk.ac.ed.ph.jqtiplus.node.ForeignElement; import uk.ac.ed.ph.jqtiplus.node.QtiNode; import uk.ac.ed.ph.jqtiplus.node.content.basic.TextRun; 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.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.template.declaration.TemplateDeclaration; import uk.ac.ed.ph.jqtiplus.node.outcome.declaration.OutcomeDeclaration; import uk.ac.ed.ph.jqtiplus.node.test.AssessmentSection; import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode; import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback; import uk.ac.ed.ph.jqtiplus.node.test.TestFeedbackAccess; 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.AssessmentSectionSessionState; import uk.ac.ed.ph.jqtiplus.state.EffectiveItemSessionControl; import uk.ac.ed.ph.jqtiplus.state.ItemSessionState; import uk.ac.ed.ph.jqtiplus.state.TestPartSessionState; import uk.ac.ed.ph.jqtiplus.state.TestPlanNode; import uk.ac.ed.ph.jqtiplus.state.TestPlanNode.TestNodeType; import uk.ac.ed.ph.jqtiplus.state.TestPlanNodeKey; import uk.ac.ed.ph.jqtiplus.state.TestSessionState; import uk.ac.ed.ph.jqtiplus.types.Identifier; import uk.ac.ed.ph.jqtiplus.value.Value; /** * * Initial date: 14.09.2015<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRenderer { private static final OLog log = Tracing.createLoggerFor(AssessmentTestComponentRenderer.class); @Override public void render(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator, RenderResult renderResult, String[] args) { AssessmentTestComponent cmp = (AssessmentTestComponent)source; TestSessionController testSessionController = cmp.getTestSessionController(); if(testSessionController.getTestSessionState().isEnded()) { renderTerminated(sb, translator); } else { /* Create appropriate options that link back to this controller */ TestSessionState testSessionState = testSessionController.getTestSessionState(); CandidateSessionContext candidateSessionContext = cmp.getCandidateSessionContext(); final AssessmentTestSession candidateSession = candidateSessionContext.getCandidateSession(); if (candidateSession.isExploded()) { renderExploded(sb, translator); } else if (candidateSessionContext.isTerminated()) { renderTerminated(sb, translator); } else { /* Touch the session's duration state if appropriate */ if (testSessionState.isEntered() && !testSessionState.isEnded()) { final Date timestamp = candidateSessionContext.getCurrentRequestTimestamp(); testSessionController.touchDurations(timestamp); } /* Render event */ AssessmentRenderer renderHints = new AssessmentRenderer(renderer); renderTestEvent(testSessionController, renderHints, sb, cmp, ubu, translator); } } } private void renderTestEvent(TestSessionController testSessionController, AssessmentRenderer renderer, StringOutput target, AssessmentTestComponent component, URLBuilder ubu, Translator translator) { CandidateSessionContext candidateSessionContext = component.getCandidateSessionContext(); CandidateEvent candidateEvent = candidateSessionContext.getLastEvent(); CandidateTestEventType testEventType = candidateEvent.getTestEventType(); /* If session has terminated, render appropriate state and exit */ final TestSessionState testSessionState = testSessionController.getTestSessionState(); if (candidateSessionContext.isTerminated() || testSessionState.isExited()) { renderTerminated(target, translator); } else if (testEventType == CandidateTestEventType.REVIEW_ITEM) { renderer.setReviewMode(true); TestPlanNodeKey itemKey = extractTargetItemKey(candidateEvent); RenderingRequest options = RenderingRequest.getItemReview(); renderTestItem(renderer, target, component, itemKey, ubu, translator, options); } else if (testEventType == CandidateTestEventType.SOLUTION_ITEM) { renderer.setSolutionMode(true); TestPlanNodeKey itemKey = extractTargetItemKey(candidateEvent); RenderingRequest options = RenderingRequest.getItemSolution(); renderTestItem(renderer, target, component, itemKey, ubu, translator, options); } else { /* Render current state */ final TestPlanNodeKey currentTestPartKey = testSessionState.getCurrentTestPartKey(); if (testSessionState.isEnded()) { /* At end of test, so show overall test feedback */ renderTestPartFeedback(renderer, target, component, ubu, translator); } else if (currentTestPartKey != null) { final TestPartSessionState currentTestPartSessionState = testSessionState.getTestPartSessionStates().get(currentTestPartKey); final TestPlanNodeKey currentItemKey = testSessionState.getCurrentItemKey(); if (currentItemKey != null) { /* An item is selected, so render it in appropriate state */ RenderingRequest options = RenderingRequest.getItem(testSessionController); renderTestItem(renderer, target, component, currentItemKey, ubu, translator, options); } else { /* No item selected */ if (currentTestPartSessionState.isEnded()) { /* testPart has ended, so must be showing testPart feedback */ renderTestPartFeedback(renderer, target, component, ubu, translator); } else { /* testPart not ended, so we must be showing the navigation menu in nonlinear mode */ renderNavigation(renderer, target, component, ubu, translator); } } } else { /* No current testPart == start of multipart test */ renderTestEntry(target, component, translator); } } } private void renderTestEntry(StringOutput sb, AssessmentTestComponent component, Translator translator) { int numOfParts = component.getAssessmentTest().getTestParts().size(); sb.append("<h4>").append(translator.translate("test.entry.page.title")).append("</h4>") .append("<div class='o_info'>") .append(translator.translate("test.entry.page.text", new String[]{ Integer.toString(numOfParts) })) .append("</div><div class='o_button_group'>"); //precondition -> up to String title = translator.translate("assessment.test.enter.test"); renderControl(sb, component, title, true, "o_sel_enter_test", new NameValuePair("cid", Event.advanceTestPart.name())); sb.append("</div>"); } private void renderControl(StringOutput sb, AssessmentTestComponent component, String title, boolean primary, String cssClass, NameValuePair... pairs) { Form form = component.getQtiItem().getRootForm(); String dispatchId = component.getQtiItem().getFormDispatchId(); sb.append("<button type='button' onclick=\""); sb.append(FormJSHelper.getXHRFnCallFor(form, dispatchId, 1, true, true, pairs)) .append(";\" class='btn ").append("btn-primary ", "btn-default ", primary).append(cssClass).append("'").append("><span>").append(title).append("</span></button>"); } private void renderTestItem(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNodeKey itemRefKey, URLBuilder ubu, Translator translator, RenderingRequest options) { final TestSessionController testSessionController = component.getTestSessionController(); final TestSessionState testSessionState = testSessionController.getTestSessionState(); String key = itemRefKey.toString(); /* We finally do the transform on the _item_ (NB!) */ sb.append("<div class='qtiworks o_assessmentitem o_assessmenttest'>"); //test part feedback 'during' //test feedback 'during' TestPlanNode itemRefNode = testSessionState.getTestPlan().getNode(itemRefKey); final EffectiveItemSessionControl effectiveItemSessionControl = itemRefNode.getEffectiveItemSessionControl(); final boolean allowComments = effectiveItemSessionControl.isAllowComment() && component.isPersonalNotes(); renderer.setCandidateCommentAllowed(allowComments); //write section rubric renderSectionRubrics(renderer, sb, component, itemRefNode, ubu, translator); // test part -> section -> item renderTestItemBody(renderer, sb, component, itemRefNode, ubu, translator, options); //controls sb.append("<div class='o_button_group o_assessmentitem_controls'>"); //submit button final ItemSessionState itemSessionState = component.getItemSessionState(itemRefNode.getKey()); if(component.isItemSessionOpen(itemSessionState, options.isSolutionMode())) { Component submit = component.getQtiItem().getSubmitButton().getComponent(); submit.getHTMLRendererSingleton().render(renderer.getRenderer(), sb, submit, ubu, translator, new RenderResult(), null); } //advanceTestItemAllowed /* && testSessionState.getCurrentItemKey() != null && testSessionController.mayAdvanceItemLinear() */ if(options.isAdvanceTestItemAllowed() ) {//TODO need to find if there is a next question String title = translator.translate("assessment.test.nextQuestion"); renderControl(sb, component, title, false, "o_sel_next_question", new NameValuePair("cid", Event.finishItem.name())); } //nextItem if(options.isNextItemAllowed() && testSessionController.hasFollowingNonLinearItem()) { String title = translator.translate("assessment.test.nextQuestion"); renderControl(sb, component, title, false, "o_sel_next_question", new NameValuePair("cid", Event.nextItem.name())); } //testPartNavigationAllowed" if(options.isTestPartNavigationAllowed() && component.isRenderNavigation()) { String title = translator.translate("assessment.test.questionMenu"); renderControl(sb, component, title, false, "o_sel_question_menu", new NameValuePair("cid", Event.testPartNavigation.name())); } //endTestPartAllowed if(options.isEndTestPartAllowed()) { String title = component.hasMultipleTestParts() ? translator.translate("assessment.test.end.testPart") : translator.translate("assessment.test.end.test"); renderControl(sb, component, title, false, "o_sel_end_testpart", new NameValuePair("cid", Event.endTestPart.name())); } //reviewMode if(options.isReviewMode()) { String title = translator.translate("assessment.test.backToTestFeedback"); renderControl(sb, component, title, false, "o_sel_back_test_feedback", new NameValuePair("cid", Event.reviewTestPart.name())); } // <xsl:variable name="provideItemSolutionButton" as="xs:boolean" select="$reviewMode and $showSolution and not($solutionMode)"/> if(options.isReviewMode() && effectiveItemSessionControl.isShowSolution() && !options.isSolutionMode()) { String title = translator.translate("assessment.solution.show"); renderControl(sb, component, title, false, "o_sel_show_solution", new NameValuePair("cid", Event.itemSolution.name()), new NameValuePair("item", key)); } if(options.isReviewMode() && options.isSolutionMode()) { String title = translator.translate("assessment.solution.hide"); renderControl(sb, component, title, false, "o_sel_solution_hide", new NameValuePair("cid", Event.reviewItem.name()), new NameValuePair("item", key)); } sb.append("</div>");//end controls sb.append("</div>");// end assessmentItem } private void renderSectionRubrics(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode itemRefNode, URLBuilder ubu, Translator translator) { boolean writeRubrics = false; boolean writeTitles = false; List<AssessmentSection> sectionParentLine = new ArrayList<>(); for(TestPlanNode parentNode=itemRefNode.getParent(); parentNode.getParent() != null; parentNode = parentNode.getParent()) { AssessmentSection selectedSection = component.getAssessmentSection(parentNode.getIdentifier()); if(selectedSection != null && selectedSection.getVisible()) { sectionParentLine.add(selectedSection); if(selectedSection.getRubricBlocks().size() > 0) { for(RubricBlock rubric:selectedSection.getRubricBlocks()) { if(rubric.getBlocks().size() > 0) { writeRubrics = true; } } } if(StringHelper.containsNonWhitespace(selectedSection.getTitle())) { writeTitles = true; } } } if(writeRubrics) { sb.append("<div class='o_info o_assessmentsection_rubrics'>"); //write the titles first if(writeTitles) { sb.append("<h4>"); for(int i=0; i<sectionParentLine.size(); i++) { if(i == 1) { sb.append("<small>"); } else if(i > 1) { sb.append(" / "); } sb.append(sectionParentLine.get(i).getTitle()); } if(sectionParentLine.size() > 1) { sb.append("</small>"); } sb.append("</h4>"); } for(int i=sectionParentLine.size(); i-->0; ) { AssessmentSection selectedSection = sectionParentLine.get(i); for(RubricBlock rubricBlock:selectedSection.getRubricBlocks()) { sb.append("<div class='rubric'>");//@view (candidate) rubricBlock.getBlocks().forEach((block) -> renderBlock(renderer, sb, component, null, null, block, ubu, translator)); sb.append("</div>"); } } sb.append("</div>"); } } private void renderTestItemBody(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode itemNode, URLBuilder ubu, Translator translator, RenderingRequest options) { final ItemSessionState itemSessionState = component.getItemSessionState(itemNode.getKey()); URI itemSystemId = itemNode.getItemSystemId(); ResolvedAssessmentItem resolvedAssessmentItem = component.getResolvedAssessmentTest() .getResolvedAssessmentItemBySystemIdMap().get(itemSystemId); final AssessmentItem assessmentItem = resolvedAssessmentItem.getRootNodeLookup().extractIfSuccessful(); sb.append("<div class='o_assessmentitem_wrapper'>"); //title + status sb.append("<h4 class='itemTitle'>"); renderItemStatus(sb, itemSessionState, options, translator); sb.append(StringHelper.escapeHtml(itemNode.getSectionPartTitle()), component.isShowTitles()) .append("</h4>") .append("<div id='itemBody' class='clearfix'>"); //render itemBody assessmentItem.getItemBody().getBlocks().forEach((block) -> renderBlock(renderer, sb, component, resolvedAssessmentItem, itemSessionState, block, ubu, translator)); //comment renderComment(renderer, sb, component, itemSessionState, translator); //submit button -> moved with the other buttons /*if(component.isItemSessionOpen(itemSessionState, options.isSolutionMode())) { Component submit = component.getQtiItem().getSubmitButton().getComponent(); submit.getHTMLRendererSingleton().render(renderer.getRenderer(), sb, submit, ubu, translator, new RenderResult(), null); }*/ //end body sb.append("</div>"); // Display active modal feedback (only after responseProcessing) if(component.isItemFeedbackAllowed(itemNode, assessmentItem, options)) { renderTestItemModalFeedback(renderer, sb, component, resolvedAssessmentItem, itemSessionState, ubu, translator); } sb.append("</div>"); // end wrapper } @Override protected void renderPrintedVariable(AssessmentRenderer renderer, StringOutput sb, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, PrintedVariable printedVar) { AssessmentTestComponent testCmp = (AssessmentTestComponent)component; Identifier identifier = printedVar.getIdentifier(); sb.append("<span class='printedVariable'>"); if(itemSessionState == null) { Value outcomeValue = testCmp.getTestSessionController().getTestSessionState().getOutcomeValue(identifier); if(outcomeValue != null) { OutcomeDeclaration outcomeDeclaration = testCmp.getAssessmentTest().getOutcomeDeclaration(identifier); renderPrintedVariable(renderer, sb, printedVar, outcomeDeclaration, outcomeValue); } } else { Value templateValue = itemSessionState.getTemplateValues().get(identifier); Value outcomeValue = itemSessionState.getOutcomeValues().get(identifier); if(outcomeValue != null) { OutcomeDeclaration outcomeDeclaration = resolvedAssessmentItem.getRootNodeLookup() .extractIfSuccessful().getOutcomeDeclaration(identifier); renderPrintedVariable(renderer, sb, printedVar, outcomeDeclaration, outcomeValue); } else if(templateValue != null) { TemplateDeclaration templateDeclaration = resolvedAssessmentItem.getRootNodeLookup() .extractIfSuccessful().getTemplateDeclaration(identifier); renderPrintedVariable(renderer, sb, printedVar, templateDeclaration, templateValue); } else { sb.append("(variable ").append(identifier.toString()).append(" was not found)"); } } sb.append("</span>"); } @Override protected void renderMath(AssessmentRenderer renderer, StringOutput out, AssessmentObjectComponent component, ResolvedAssessmentItem resolvedAssessmentItem, ItemSessionState itemSessionState, QtiNode mathElement) { if(resolvedAssessmentItem != null) { super.renderMath(renderer, out, component, resolvedAssessmentItem, itemSessionState, mathElement); } else if(mathElement instanceof ForeignElement) { ForeignElement fElement = (ForeignElement)mathElement; boolean mi = fElement.getQtiClassName().equals("mi"); boolean ci = fElement.getQtiClassName().equals("ci"); if(mi || ci) { String text = contentAsString(fElement); Identifier identifier = Identifier.assumedLegal(text); AssessmentTestComponent testComponent = (AssessmentTestComponent)component; Value outcomeValue = testComponent.getTestSessionController().getTestSessionState().getOutcomeValue(identifier); if(outcomeValue != null) { if(ci) { substituteCi(out, outcomeValue); } else if(mi) { substituteMi(out, outcomeValue); } } 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()); } } private void renderTestPartFeedback(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, URLBuilder ubu, Translator translator) { sb.append("<div class='qtiworks o_assessmenttest testFeedback'>") .append("<h1>"); if(component.hasMultipleTestParts()) { sb.append(translator.translate("test.part.complete")); } else { sb.append(translator.translate("test.complete")); } sb.append("</h1>"); // Show 'atEnd' testPart feedback TestPlanNode currentTestPartNode = component.getCurrentTestPartNode(); TestPart currentTestPart = component.getTestPart(currentTestPartNode.getIdentifier()); renderTestFeebacks(renderer, sb, currentTestPart.getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator); //Show 'atEnd' test feedback f there's only 1 testPart //if(!component.hasMultipleTestParts()) { renderTestFeebacks(renderer, sb, component.getAssessmentTest().getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator); //} //test part review component.getTestSessionController().getTestSessionState().getTestPlan() .getTestPartNodes().forEach((testPartNode) -> renderReview(renderer, sb, component, testPartNode, ubu, translator)); //controls /* sb.append("<div class='o_button_group'>"); String title = component.hasMultipleTestParts() ? translator.translate("assessment.test.end.testPart") : translator.translate("assessment.test.end.test"); renderControl(sb, component, title, "o_sel_end_testpart", new NameValuePair("cid", Event.advanceTestPart.name())); sb.append("</div>"); */ sb.append("</div>"); } private void renderReview(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode node, URLBuilder ubu, Translator translator) { switch(node.getTestNodeType()) { case TEST_PART: renderReviewTestPart(renderer, sb, component, node, ubu, translator); break; case ASSESSMENT_SECTION: renderReviewAssessmentSection(renderer, sb, component, node, ubu, translator); break; case ASSESSMENT_ITEM_REF: renderReviewAssessmentItem(sb, component, node, translator); break; default: break; } } private void renderReviewTestPart(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode node, URLBuilder ubu, Translator translator) { if(component.isRenderNavigation() || true) { //".//qw:node[@type='ASSESSMENT_ITEM_REF' and (@allowReview='true' or @showFeedback='true')]" as="element(qw:node)*"/> boolean hasReviewableItems = node.searchDescendants(TestNodeType.ASSESSMENT_ITEM_REF) .stream().anyMatch(itemNode -> itemNode.getEffectiveItemSessionControl().isAllowReview() || itemNode.getEffectiveItemSessionControl().isShowFeedback()); if(hasReviewableItems) { sb.append("<h4>").append(translator.translate("review.responses")).append("</h4>"); sb.append("<p>").append(translator.translate("review.responses.desc")).append("</p>"); sb.append("<div class='o_qti_menu_buttonstyle'>"); sb.append("<ul class='o_testpartnavigation'>"); node.getChildren().forEach((childNode) -> renderReview(renderer, sb, component, childNode, ubu, translator)); sb.append("</ul></div>"); } } } private void renderReviewAssessmentSection(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode sectionNode, URLBuilder ubu, Translator translator) { AssessmentSectionSessionState assessmentSessionSessionState = component.getTestSessionController() .getTestSessionState().getAssessmentSectionSessionStates().get(sectionNode.getKey()); TestPart currentTestPart = component.getTestPart(component.getCurrentTestPartNode().getIdentifier()); //<xsl:if test="$currentTestPart/@navigationMode='nonlinear' or exists($assessmentSessionSessionState/@entryTime)"> if(currentTestPart.getNavigationMode() == NavigationMode.NONLINEAR || assessmentSessionSessionState.getEntryTime() != null) { sb.append("<li class='o_assessmentsection'>") .append("<header><h2>") .append(StringHelper.escapeHtml(sectionNode.getSectionPartTitle())).append("</h2>"); renderAssessmentSectionRubrickBlock(renderer, sb, component, sectionNode, ubu, translator); sb.append("</header>"); sb.append("<ul class='o_testpartnavigation_inner list-unstyled'>"); sectionNode.getChildren().forEach((childNode) -> renderReview(renderer, sb, component, childNode, ubu, translator)); sb.append("</ul>"); } } private void renderReviewAssessmentItem(StringOutput sb, AssessmentTestComponent component, TestPlanNode itemNode, Translator translator) { EffectiveItemSessionControl itemSessionControl = itemNode.getEffectiveItemSessionControl(); //<xsl:variable name="reviewable" select="@allowReview='true' or @showFeedback='true'" as="xs:boolean"/> boolean reviewable = itemSessionControl.isAllowReview() || itemSessionControl.isShowFeedback(); //<xsl:variable name="itemSessionState" select="$testSessionState/qw:item[@key=current()/@key]/qw:itemSessionState" as="element(qw:itemSessionState)"/> ItemSessionState itemSessionState = component.getTestSessionController().getTestSessionState().getItemSessionStates().get(itemNode.getKey()); //<xsl:if test="$currentTestPart/@navigationMode='nonlinear' or exists($itemSessionState/@entryTime)"> TestPart currentTestPart = component.getTestPart(component.getCurrentTestPartNode().getIdentifier()); if(currentTestPart.getNavigationMode() == NavigationMode.NONLINEAR || itemSessionState.getEntryTime() != null) { sb.append("<li class='o_assessmentitem'>"); sb.append("<button type='button' onclick=\""); String key = itemNode.getKey().toString(); sb.append(FormJSHelper.getXHRFnCallFor(component.getQtiItem(), true, true, new NameValuePair("cid", Event.reviewItem.name()), new NameValuePair("item", key))); sb.append(";\" class='btn btn-default' ").append(" disabled", !reviewable).append("><span class='questionTitle'>") .append(StringHelper.escapeHtml(itemNode.getSectionPartTitle())).append("</span>"); if(!reviewable) { renderItemStatusMessage("reviewNotAllowed", "assessment.item.status.reviewNot", sb, translator); } else if(itemSessionState.getUnboundResponseIdentifiers().size() > 0 || itemSessionState.getInvalidResponseIdentifiers().size() > 0) { 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); } sb.append("</button></li>"); } } private void renderTestFeebacks(AssessmentRenderer renderer, StringOutput sb, List<TestFeedback> testFeedbacks, AssessmentTestComponent component, TestFeedbackAccess access, URLBuilder ubu, Translator translator) { for(TestFeedback testFeedback:testFeedbacks) { if(testFeedback.getTestFeedbackAccess() == access) { renderTestFeeback(renderer, sb, component, testFeedback, ubu, translator); } } } private void renderTestFeeback(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestFeedback testFeedback, URLBuilder ubu, Translator translator) { TestSessionState testSessionState = component.getTestSessionController().getTestSessionState(); if(testFeedbackVisible(testFeedback, testSessionState)) { sb.append("<div class='o_info clearfix'>"); sb.append("<h3>"); if(StringHelper.containsNonWhitespace(testFeedback.getTitle())) { sb.append(StringHelper.escapeHtml(testFeedback.getTitle())); } else { sb.append(translator.translate("assessment.test.modal.feedback")); } sb.append("</h3>"); testFeedback.getChildren().forEach((flow) -> renderFlow(renderer, sb, component, null, null, flow, ubu, translator)); sb.append("</div>"); } } private void renderNavigation(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, URLBuilder ubu, Translator translator) { if(component.isRenderNavigation()) { sb.append("<div id='o_qti_menu' class='qtiworks o_assessmenttest o_testpartnavigation o_qti_menu_buttonstyle'>"); //title boolean multiPartTest = component.hasMultipleTestParts(); String title = multiPartTest ? translator.translate("assessment.test.nav.title.multiPartTestMenu") : translator.translate("assessment.test.nav.title.questionMenu"); sb.append("<h3>").append(title).append("</h3>"); //part, sections and item refs sb.append("<ul class='o_testpartnavigation list-unstyled'>"); component.getCurrentTestPartNode().getChildren().forEach((node) -> renderNavigation(renderer, sb, component, node, ubu, translator)); sb.append("</ul>"); // test controls TestSessionController testSessionController = component.getTestSessionController(); boolean allowedToEndTestPart = testSessionController.getTestSessionState().getCurrentTestPartKey() != null && testSessionController.mayEndCurrentTestPart(); sb.append("<div class='o_button_group'>"); sb.append("<button type='button' onclick=\""); if(allowedToEndTestPart) { Form form = component.getQtiItem().getRootForm(); String dispatchId = component.getQtiItem().getFormDispatchId(); sb.append(FormJSHelper.getXHRFnCallFor(form, dispatchId, 1, true, true, new NameValuePair("cid", Event.endTestPart.name()))); } else { sb.append("javascript:"); } String endTestTitle = multiPartTest ? translator.translate("assessment.test.end.testPart") : translator.translate("assessment.test.end.test"); sb.append(";\" class='btn btn-default o_sel_end_testpart'").append(" disabled", !allowedToEndTestPart).append("><span>") .append(endTestTitle).append("</span>"); sb.append("</button>"); sb.append("</div></div>"); } } private void renderNavigation(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode node, URLBuilder ubu, Translator translator) { switch(node.getTestNodeType()) { case ASSESSMENT_SECTION: renderNavigationAssessmentSection(renderer, sb, component, node, ubu, translator); break; case ASSESSMENT_ITEM_REF: renderNavigationAssessmentItem(sb, component, node, translator); break; default: break; } } private void renderNavigationAssessmentSection(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode sectionNode, URLBuilder ubu, Translator translator) { sb.append("<li class='o_assessmentsection o_qti_menu_item'>") .append("<header><h4>").append(StringHelper.escapeHtml(sectionNode.getSectionPartTitle())).append("</h4>"); renderAssessmentSectionRubrickBlock(renderer, sb, component, sectionNode, ubu, translator); sb.append("</header><ul class='o_testpartnavigation_inner list-unstyled'>"); sectionNode.getChildren().forEach((child) -> renderNavigation(renderer, sb, component, child, ubu, translator)); sb.append("</ul></li>"); } private void renderAssessmentSectionRubrickBlock(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestPlanNode sectionNode, URLBuilder ubu, Translator translator) { AssessmentSection selectedSection = component.getAssessmentSection(sectionNode.getIdentifier()); if(selectedSection != null && selectedSection.getRubricBlocks().size() > 0) { for(RubricBlock rubricBlock:selectedSection.getRubricBlocks()) { sb.append("<div class='rubric'>");//@view (candidate) rubricBlock.getBlocks().forEach((block) -> renderBlock(renderer, sb, component, null, null, block, ubu, translator)); sb.append("</div>"); } } } 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>"); } private void renderNavigationAssessmentItem(StringOutput sb, AssessmentTestComponent component, TestPlanNode itemNode, Translator translator) { String key = itemNode.getKey().toString(); sb.append("<li class='o_assessmentitem'>"); sb.append("<button type='button' onclick=\""); Form form = component.getQtiItem().getRootForm(); String dispatchId = component.getQtiItem().getFormDispatchId(); sb.append(FormJSHelper.getXHRFnCallFor(form, dispatchId, 1, true, true, new NameValuePair("cid", Event.selectItem.name()), new NameValuePair("item", key))); sb.append(";\" class='btn btn-default'><span class='questionTitle'>") .append(StringHelper.escapeHtml(itemNode.getSectionPartTitle())).append("</span>"); ItemSessionState itemSessionState = component.getItemSessionState(itemNode.getKey()); if(itemSessionState.getEndTime() != null) { renderItemStatusMessage("ended", "assessment.item.status.finished", sb, translator); } else if(itemSessionState.getUnboundResponseIdentifiers().size() > 0 || itemSessionState.getInvalidResponseIdentifiers().size() > 0) { renderItemStatusMessage("invalid", "assessment.item.status.needsAttention", sb, translator); } else if(itemSessionState.isResponded() || itemSessionState.hasUncommittedResponseValues()) { 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); } sb.append("</button>"); sb.append("</li>"); } public static void printDocument(Element doc, OutputStream out) { try { TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); transformer.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(out, "UTF-8"))); } catch (IllegalArgumentException | UnsupportedEncodingException | TransformerFactoryConfigurationError | TransformerException e) { log.error("", e); } } private TestPlanNodeKey extractTargetItemKey(final CandidateEvent candidateEvent) { final String keyString = candidateEvent.getTestItemKey(); try { return TestPlanNodeKey.fromString(keyString); } catch (final Exception e) { throw new OLATRuntimeException("Unexpected Exception parsing TestPlanNodeKey " + keyString, e); } } }