/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <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>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.modules.iq;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import org.apache.commons.lang.StringEscapeUtils;
import org.dom4j.Element;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.ComponentRenderer;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.render.RenderResult;
import org.olat.core.gui.render.Renderer;
import org.olat.core.gui.render.RenderingState;
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.util.Formatter;
import org.olat.ims.qti.QTIConstants;
import org.olat.ims.qti.container.AssessmentContext;
import org.olat.ims.qti.container.ItemContext;
import org.olat.ims.qti.container.Output;
import org.olat.ims.qti.container.SectionContext;
import org.olat.ims.qti.container.qtielements.GenericQTIElement;
import org.olat.ims.qti.container.qtielements.Hint;
import org.olat.ims.qti.container.qtielements.Item;
import org.olat.ims.qti.container.qtielements.ItemFeedback;
import org.olat.ims.qti.container.qtielements.Material;
import org.olat.ims.qti.container.qtielements.Objectives;
import org.olat.ims.qti.container.qtielements.RenderInstructions;
import org.olat.ims.qti.container.qtielements.Solution;
import org.olat.ims.qti.navigator.Info;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.Resolver;
/**
*
* @author Felix Jost
*/
public class IQComponentRenderer implements ComponentRenderer {
/**
* Default constructor
*/
public IQComponentRenderer() {
super();
}
/**
* Render the QTI form
* @param comp
* @param translator
* @param renderer
* @return rendered form
*/
public StringOutput buildForm(IQComponent comp, Translator translator, Renderer renderer, URLBuilder ubu) {
StringOutput sb = new StringOutput();
Info info = comp.getAssessmentInstance().getNavigator().getInfo();
AssessmentInstance ai = comp.getAssessmentInstance();
int status = info.getStatus();
int message = info.getMessage();
boolean renderItems = info.isRenderItems();
AssessmentContext act = ai.getAssessmentContext();
// first treat messages and errors
if (info.containsMessage()) {
switch (message) {
case QTIConstants.MESSAGE_ITEM_SUBMITTED :
//item hints?
if (info.isHint()) {
Hint el_hint = info.getCurrentOutput().getHint();
if (el_hint.getFeedbackstyle() == Hint.FEEDBACKSTYLE_INCREMENTAL) {
// increase the hint level so we know which hint to display
ItemContext itc = act.getCurrentSectionContext().getCurrentItemContext();
int nLevel = itc.getHintLevel() + 1;
int numofhints = el_hint.getChildCount();
if (nLevel > numofhints) nLevel = numofhints;
itc.setHintLevel(nLevel);
//<!ELEMENT hint (qticomment? , hintmaterial+)>
displayFeedback(sb, (GenericQTIElement)el_hint.getChildAt(nLevel-1), ai, translator.getLocale());
} else {
displayFeedback(sb, el_hint, ai, translator.getLocale());
}
}
//item solution?
if (info.isSolution()) {
Solution el_solution = info.getCurrentOutput().getSolution();
displayFeedback(sb, el_solution, ai, translator.getLocale());
}
// item fb?
if (info.isFeedback()) {
if (info.getCurrentOutput().hasItem_Responses()) {
int fbcount = info.getCurrentOutput().getFeedbackCount();
int i=0;
while (i < fbcount) {
Element el_anschosen = info.getCurrentOutput().getItemAnswerChosen(i);
if (el_anschosen != null) {
sb.append("<br /><br /><i>");
displayFeedback(sb, new Material(el_anschosen), ai, translator.getLocale());
sb.append("</i>");
}
Element el_resp= info.getCurrentOutput().getItemFeedback(i);
displayFeedback(sb, new ItemFeedback(el_resp), ai, translator.getLocale());
i++;
}
}
}
if(!comp.getMenuDisplayConf().isEnabledMenu() && comp.getMenuDisplayConf().isItemPageSequence() && !info.isRenderItems()) {
//if item was submitted and sequence is pageSequence and menu not enabled and isRenderItems returns false show section info
SectionContext sc = ai.getAssessmentContext().getCurrentSectionContext();
displaySectionInfo(sb, sc, ai, comp, ubu, translator);
}
break;
case QTIConstants.MESSAGE_SECTION_SUBMITTED :
// provide section feedback if enabled and existing
//SectionContext sc = act.getCurrentSectionContext();
if (info.isFeedback()) {
Output outp = info.getCurrentOutput();
GenericQTIElement el_feedback = outp.getEl_response();
if (el_feedback != null) displayFeedback(sb, el_feedback, ai, translator.getLocale());
}
if(!comp.getMenuDisplayConf().isEnabledMenu() && !comp.getMenuDisplayConf().isItemPageSequence()) {
SectionContext sc = ai.getAssessmentContext().getCurrentSectionContext();
displaySectionInfo(sb, sc, ai, comp, ubu, translator);
}
break;
case QTIConstants.MESSAGE_ASSESSMENT_SUBMITTED :
// provide assessment feedback if enabled and existing
if (info.isFeedback()) {
Output outp = info.getCurrentOutput();
GenericQTIElement el_feedback = outp.getEl_response();
if (el_feedback != null) displayFeedback(sb, el_feedback, ai, translator.getLocale());
}
break;
case QTIConstants.MESSAGE_SECTION_INFODEMANDED : // for menu item navigator
// provide some stats maybe
SectionContext sc = ai.getAssessmentContext().getCurrentSectionContext();
displaySectionInfo(sb, sc, ai, comp, ubu, translator);
break;
case QTIConstants.MESSAGE_ASSESSMENT_INFODEMANDED : // at the start of the test
displayAssessmentInfo(sb, act, ai, comp, ubu, translator);
break;
}
}
if (renderItems) {
boolean displayForm = true;
// First check wether we need to render a form.
// No form is needed if the current item has a matapplet object to be displayed.
// Matapplets will send their response back directly.
SectionContext sct = act.getCurrentSectionContext();
ItemContext itc = null;
if (sct != null && !ai.isSectionPage()) {
itc = sct.getCurrentItemContext();
if (itc != null) {
Item item = itc.getQtiItem();
if (item.getQTIIdent().startsWith("QTIEDIT:FLA:")) displayForm = false;
}
}
sb.append("<form action=\"");
ubu.buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { "sitse" });
sb.append("\" id=\"ofo_iq_item\" method=\"post\">");
if (!ai.isSectionPage()) {
if (itc != null) displayItem(sb, renderer, ubu, itc, ai);
} else {
if (sct != null && sct.getItemContextCount() != 0)
displayItems(sb, renderer, ubu, sct, ai);
}
sb.append("<div class=\"b_button_group\"><input class=\"b_button\" type=\"submit\" name=\"olat_fosm\" value=\"");
if (ai.isSectionPage())
sb.append(StringEscapeUtils.escapeHtml(translator.translate("submitMultiAnswers")));
else
sb.append(StringEscapeUtils.escapeHtml(translator.translate("submitSingleAnswer")));
sb.append("\"");
if (!displayForm) sb.append(" style=\"display: none;\"");
sb.append(" /></div></form>");
}
if (status == QTIConstants.ASSESSMENT_FINISHED) {
if (info.isFeedback()) {
Output outp = info.getCurrentOutput();
GenericQTIElement el_feedback = outp.getEl_response();
if (el_feedback != null) displayFeedback(sb, el_feedback, ai, null);
}
}
return sb;
}
protected static String getFormattedLimit(long millis) {
long sSec = millis / 1000;
long sMin = sSec / 60;
sSec = sSec - (sMin * 60);
StringOutput sb = new StringOutput();
sb.append(sMin);
sb.append("' ");
sb.append(sSec);
sb.append("\"");
return sb.toString();
}
private StringOutput addItemLink(Renderer r, URLBuilder ubu ,Formatter formatter, ItemContext itc, int sectionPos, int itemPos,
boolean clickable, boolean active, boolean info) {
StringOutput sb = new StringOutput();
String title = itc.getEl_item().attributeValue("title", "no title");
String titleShort = Formatter.truncate(title, 27);
long maxdur = itc.getDurationLimit();
long start = itc.getTimeOfStart();
long due = start + maxdur;
boolean started = (start != -1);
boolean timelimit = (maxdur != -1);
String fdue = (started && timelimit ? formatter.formatTimeShort(new Date(due)) : null);
sb.append("<div class=\"o_qti_menu_item\">");
if (clickable) {
sb.append("<a onclick=\"return o2cl()\" href=\"");
ubu.buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { "git" });
sb.append("?itid=" + itemPos + "&seid=" + sectionPos);
sb.append("\" title=\"" + StringEscapeUtils.escapeHtml(title) + "\">");
}
sb.append("<b>" + (sectionPos + 1) + "." + (itemPos + 1) + ".</b> ");
if (active) {
sb.append("<strong>" + titleShort + "</strong>");
} else {
sb.append(titleShort);
}
if (clickable) {
sb.append("</a>");
}
sb.append("</div>");
if (!itc.isOpen()) {
// add lock image
sb.append("<div class=\"b_small_icon o_qti_closed_icon\" title=\"Closed\" ></div>");
} else if (info) {
// max duration info
if (maxdur != -1) {
sb.append("<div class=\"b_small_icon o_qti_timelimit_icon\" title=\"");
if (!itc.isStarted()) {
sb.append(StringEscapeUtils.escapeHtml(r.getTranslator().translate("timelimit.initial", new String[] {getFormattedLimit(maxdur)})));
} else {
sb.append(StringEscapeUtils.escapeHtml(r.getTranslator().translate("timelimit.running", new String[] {fdue})));
}
sb.append("\" ></div>");
}
// attempts info
int maxa = itc.getMaxAttempts();
int attempts = itc.getTimesAnswered();
if (maxa != -1) { // only limited times of answers
sb.append("<div class=\"b_small_icon o_qti_attemptslimit_icon\" title=\"");
sb.append(StringEscapeUtils.escapeHtml(r.getTranslator().translate("attemptsleft", new String[] {"" + (maxa - attempts)})));
sb.append("\" ></div>");
}
}
return sb;
}
// menu stuff
private StringOutput addSectionLink(Renderer r, URLBuilder ubu, Formatter formatter, SectionContext sc, int sectionPos, boolean clickable, boolean active) {
StringOutput sb = new StringOutput();
// section link
String title = Formatter.truncate(sc.getTitle(), 30);
long maxdur = sc.getDurationLimit();
long start = sc.getTimeOfStart();
long due = start + maxdur;
boolean started = (start != -1);
boolean timelimit = (maxdur != -1);
String fdue = (started && timelimit ? formatter.formatTimeShort(new Date(due)) : null);
sb.append("<div class=\"o_qti_menu_section\">");
if (!sc.isOpen()) clickable = false;
if (clickable) {
sb.append("<a onclick=\"return o2cl()\" href=\"");
ubu.buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { "gse" });
sb.append("?seid=" + sectionPos);
sb.append("\" title=\"" + StringEscapeUtils.escapeHtml(sc.getTitle()) + "\">");
}
sb.append("<b>" + (sectionPos + 1) + ".</b> ");
if (active) {
sb.append("<strong>" + title + "</strong>");
} else {
sb.append(title);
}
if (clickable) {
sb.append("</a>");
}
sb.append("</div>");
if (!sc.isOpen()) {
// add lock image
sb.append("<div class=\"b_small_icon o_qti_closed_icon\" title=\"Section is closed.\" ></div>");
} else {
// max duration info
if (maxdur != -1) {
sb.append("<div class=\"b_small_icon o_qti_timelimit_icon\" title=\"");
if (!sc.isStarted()) {
sb.append(StringEscapeUtils.escapeHtml(r.getTranslator().translate("timelimit.initial", new String[] {getFormattedLimit(maxdur)})));
} else {
sb.append(StringEscapeUtils.escapeHtml(r.getTranslator().translate("timelimit.running", new String[] {fdue})));
}
sb.append("\" ></div>");
}
}
return sb;
}
/**
* Method buildMenu.
*
* @return DOCUMENT ME!
*/
private StringOutput buildMenu(IQComponent comp, Translator translator, Renderer r, URLBuilder ubu) {
StringOutput sb = new StringOutput();
AssessmentInstance ai = comp.getAssessmentInstance();
AssessmentContext ac = ai.getAssessmentContext();
boolean renderSectionTitlesOnly = comp.getMenuDisplayConf().isRenderSectionsOnly();
sb.append("<h4>");
sb.append(ac.getTitle());
sb.append("</h4>");
// append assessment navigation
Formatter formatter = Formatter.getInstance(translator.getLocale());
int scnt = ac.getSectionContextCount();
for (int i = 0; i < scnt; i++) {
SectionContext sc = ac.getSectionContext(i);
boolean clickable = (ai.isSectionPage() && sc.isOpen()) || (!ai.isSectionPage());
clickable = clickable && !ai.isClosed();
clickable = clickable && ai.isMenu();
sb.append("<ul><li class=\"o_qti_menu_section\">");
sb.append(addSectionLink(r, ubu, formatter, sc, i, clickable, ac.getCurrentSectionContextPos() == i));
sb.append("</li>");
if (!renderSectionTitlesOnly) {
//not only sections, but render questions to
int icnt = sc.getItemContextCount();
for (int j = 0; j < icnt; j++) {
ItemContext itc = sc.getItemContext(j);
clickable = !ai.isSectionPage() && sc.isOpen() && itc.isOpen();
clickable = clickable && !ai.isClosed();
clickable = clickable && ai.isMenu();
sb.append("<li class=\"o_qti_menu_item\">");
sb.append(addItemLink(r, ubu, formatter, itc, i, j, clickable,
(ac.getCurrentSectionContextPos() == i && sc.getCurrentItemContextPos() == j), !ai.isSurvey()));
sb.append("</li>");
}
}
sb.append("</ul>");
}
return sb;
}
private void displayItems(StringOutput sb, Renderer renderer, URLBuilder ubu, SectionContext sc, AssessmentInstance ai) {
// display the whole current section on one page
List items = sc.getItemContextsToRender();
for (Iterator iter= items.iterator(); iter.hasNext();) {
ItemContext itc = (ItemContext) iter.next();
if (itc.isOpen()) {
displayItem(sb, renderer, ubu, itc, ai);
} else {
displayItemClosed(sb,renderer,itc);
}
}
}
/**
* Display message : Item is closed, could not be displayed.
* @param sb
* @param renderer
* @param itc
*/
private void displayItemClosed(StringOutput sb, Renderer renderer,ItemContext itc) {
StringBuilder buffer = new StringBuilder(100);
buffer.append("<div class=\"b_warning\"><strong>").append(renderer.getTranslator().translate("couldNotDisplayItem")).append("</strong></div>");
sb.append(buffer);
}
private void displayItem(StringOutput sb, Renderer renderer, URLBuilder ubu, ItemContext itc, AssessmentInstance ai) {
StringBuilder buffer = new StringBuilder(1000);
Resolver resolver = ai.getResolver();
RenderInstructions ri = new RenderInstructions();
ri.put(RenderInstructions.KEY_STATICS_PATH, resolver.getStaticsBaseURI() + "/");
ri.put(RenderInstructions.KEY_LOCALE, renderer.getTranslator().getLocale());
StringOutput soCommandURI = new StringOutput(50);
ubu.buildURI(soCommandURI, new String[] { VelocityContainer.COMMAND_ID }, new String[] { "sflash" });
ri.put(RenderInstructions.KEY_APPLET_SUBMIT_URI, soCommandURI.toString());
if (itc.getItemInput() != null)
ri.put(RenderInstructions.KEY_ITEM_INPUT, itc.getItemInput());
ri.put(RenderInstructions.KEY_RENDER_TITLE, Boolean.valueOf(ai.isDisplayTitles()));
itc.getQtiItem().render(buffer, ri);
sb.append(buffer);
}
private void displaySectionInfo(StringOutput sb, SectionContext sc, AssessmentInstance ai, IQComponent comp, URLBuilder ubu, Translator translator) {
// display the sectionInfo
if (sc == null) return;
if (ai.isDisplayTitles())
sb.append("<h3>" + sc.getTitle() + "</h3>");
Objectives objectives = sc.getObjectives();
if (objectives != null) {
StringBuilder sbTmp = new StringBuilder();
Resolver resolver = ai.getResolver();
RenderInstructions ri = new RenderInstructions();
ri.put(RenderInstructions.KEY_STATICS_PATH, resolver.getStaticsBaseURI() + "/");
objectives.render(sbTmp, ri);
sb.append(sbTmp);
}
// if Menu not visible, or if visible but not selectable, and itemPage sequence (one question per page)
// show button to navigate to the first question of the current section
IQMenuDisplayConf menuDisplayConfig = comp.getMenuDisplayConf();
if (!menuDisplayConfig.isEnabledMenu() && menuDisplayConfig.isItemPageSequence()) {
sb.append("<a onclick=\"return o2cl()\" href=\"");
ubu.buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { "git" });
AssessmentContext ac = ai.getAssessmentContext();
int sectionPos = ac.getCurrentSectionContextPos();
sb.append("?itid=" + 0 + "&seid=" + sectionPos);
String title = translator.translate("next");
sb.append("\" title=\"" + StringEscapeUtils.escapeHtml(title) + "\">");
sb.append(title);
sb.append("</a>");
}
}
private void displayAssessmentInfo(StringOutput sb, AssessmentContext ac, AssessmentInstance ai, IQComponent comp, URLBuilder ubu, Translator translator) {
Objectives objectives = ac.getObjectives();
if (objectives != null) {
StringBuilder sbTmp = new StringBuilder();
Resolver resolver = ai.getResolver();
RenderInstructions ri = new RenderInstructions();
ri.put(RenderInstructions.KEY_STATICS_PATH, resolver.getStaticsBaseURI() + "/");
objectives.render(sbTmp, ri);
sb.append(sbTmp);
}
//if Menu not visible, or if visible but not selectable show button to navigate to the first section panel
IQMenuDisplayConf menuDisplayConfig = comp.getMenuDisplayConf();
if (!menuDisplayConfig.isEnabledMenu()) {
sb.append("<a onclick=\"return o2cl()\" href=\"");
ubu.buildURI(sb, new String[] { VelocityContainer.COMMAND_ID }, new String[] { "gse" });
sb.append("?seid=" + 0);
String title = translator.translate("next");
sb.append("\" title=\"" + StringEscapeUtils.escapeHtml(title) + "\">");
sb.append(title);
sb.append("</a>");
}
}
private void displayFeedback(StringOutput sb, GenericQTIElement feedback, AssessmentInstance ai, Locale locale) {
StringBuilder sbTmp = new StringBuilder();
Resolver resolver = ai.getResolver();
RenderInstructions ri = new RenderInstructions();
ri.put(RenderInstructions.KEY_STATICS_PATH, resolver.getStaticsBaseURI() + "/");
ri.put(RenderInstructions.KEY_LOCALE, locale);
feedback.render(sbTmp, ri);
sb.append(sbTmp);
}
/**
* @see org.olat.core.gui.render.ui.ComponentRenderer#render(org.olat.core.gui.render.Renderer, org.olat.core.gui.render.StringOutput, org.olat.core.gui.components.Component, org.olat.core.gui.render.URLBuilder, org.olat.core.gui.translator.Translator, org.olat.core.gui.render.RenderResult, java.lang.String[])
*/
public void render(Renderer renderer, StringOutput target, Component source, URLBuilder ubu, Translator translator, RenderResult renderResult, String[] args) {
IQComponent qticomp = (IQComponent)source;
if (args[0].equals("menu")) { // render the menu
target.append(buildMenu(qticomp, translator, renderer, ubu));
} else if (args[0].equals("qtiform")) { // render the content
target.append(buildForm(qticomp, translator, renderer, ubu));
}
}
/**
* @see org.olat.core.gui.render.ui.ComponentRenderer#renderHeaderIncludes(org.olat.core.gui.render.Renderer, org.olat.core.gui.render.StringOutput, org.olat.core.gui.components.Component, org.olat.core.gui.render.URLBuilder, org.olat.core.gui.translator.Translator)
*/
public void renderHeaderIncludes(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator, RenderingState rstate) {
//
}
/**
* @see org.olat.core.gui.render.ui.ComponentRenderer#renderBodyOnLoadJSFunctionCall(org.olat.core.gui.render.Renderer, org.olat.core.gui.render.StringOutput, org.olat.core.gui.components.Component)
*/
public void renderBodyOnLoadJSFunctionCall(Renderer renderer, StringOutput sb, Component source, RenderingState rstate) {
//
}
}