/**
* 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.ims.qti.container;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.dom4j.Element;
import org.dom4j.Node;
import org.olat.ims.qti.container.qtielements.Objectives;
import org.olat.ims.qti.container.qtielements.SectionFeedback;
import org.olat.ims.qti.process.AssessmentInstance;
import org.olat.ims.qti.process.QTIHelper;
import org.olat.ims.qti.process.elements.ExpressionBuilder;
import org.olat.ims.qti.process.elements.ScoreBooleanEvaluable;
/**
* @author Potable Shop
*/
public class SectionContext implements Serializable {
private String ident;
//private String title;
private AssessmentInstance assessInstance;
// readonly ref!: the ref to the el_section; transient since it we don't want
// to serialize it (too long) and can reattach it later
private Element el_section;
private Objectives objectives;
private List itemContexts;
private float totalScore; // only floats and integers supported at the moment
private int cutvalue;
private int currentItemContextPos;
private long timeOfStart;
// -1 = not started yet; server time at the time of the start of the section
private long durationLimit; //
private long timesAnswered;
private long latestAnswerTime;
private Output output;
//private boolean outcomesProcessing;
private boolean feedbacktesting; // has the section feedback calculation
private boolean feedbackswitchedon; // is feedback allowed
private boolean feedbackavailable; // is feedback currently available?
private String scoremodel;
/**
* default constructor needed for persistence
*/
public SectionContext() {
//
}
/**
*
*/
public void init() {
totalScore = 0.0f;
currentItemContextPos = -1;
timeOfStart = -1; // not started yet
timesAnswered = 0; // not answered yet (this flag has no direct meaning in
// qti)
latestAnswerTime = -1; //outcomesProcessing = false;
feedbacktesting = false;
cutvalue = -1;
feedbackavailable = false;
}
/**
* Start duration counters.
*/
public void start() {
if (timeOfStart == -1) { // if not started already
timeOfStart = System.currentTimeMillis();
}
}
/**
*
*/
public void sectionWasSubmitted() {
timesAnswered++;
latestAnswerTime = System.currentTimeMillis();
}
/**
* Method eval.
*/
public void eval() {
if (assessInstance.isSurvey()) return;
int itccnt = getItemContextCount();
for (int i = 0; i < itccnt; i++) {
ItemContext ict = getItemContext(i);
ict.eval();
}
calcScore(); // calc feedback
if (feedbacktesting) calcFeedBack();
}
/**
* @param assessInstance
* @param el_section
* @param sw
*/
public void setUp(AssessmentInstance assessInstance, Element el_section, Switches sw) {
this.assessInstance = assessInstance;
this.el_section = el_section;
this.ident = el_section.attributeValue("ident");
init();
Element dur = (Element) el_section.selectSingleNode("duration");
if (dur == null) {
durationLimit = -1; // no limit
} else {
String sdur = dur.getText();
durationLimit = QTIHelper.parseISODuration(sdur);
if (durationLimit == 0) durationLimit = -1; // Assesst Designer fix
}
// get objectives
Element el_objectives = (Element)el_section.selectSingleNode("objectives");
if (el_objectives != null) objectives = new Objectives(el_objectives);
// feedback switches
// ---------------------------------------------------------
if (sw == null) { // no switches from the assessment context dominate
// retrieve section switches
Element el_control = (Element) el_section.selectSingleNode("sectioncontrol");
if (el_control != null) {
String feedbackswitch = el_control.attributeValue("feedbackswitch");
String hintswitch = el_control.attributeValue("hintswitch");
String solutionswitch = el_control.attributeValue("solutionswitch");
feedbackswitchedon = (feedbackswitch == null) ? true : feedbackswitch.equals("Yes");
boolean hints = (hintswitch == null) ? true : hintswitch.equals("Yes");
boolean solutions = (solutionswitch == null) ? true : solutionswitch.equals("Yes");
sw = new Switches(feedbackswitchedon, hints, solutions);
}
}
// ----------------------- selection
List el_items = new ArrayList();
// determine which items (sections not implemented) will be chosen/selected
// for this section
// --- 1. take all items and resolved itemrefs which are in the section
List items = el_section.selectNodes("item|itemref");
for (Iterator iter = items.iterator(); iter.hasNext();) {
Element el_item = (Element) iter.next();
//<!ELEMENT itemref (#PCDATA)> <!ATTLIST itemref %I_LinkRefId; > <!ENTITY
// % I_LinkRefId " linkrefid CDATA #REQUIRED">
if (el_item.getName().equals("itemref")) {
// resolve the entity first
String linkRefId = el_item.attributeValue("linkrefid");
el_item = (Element) el_section.selectSingleNode("//item[@ident='" + linkRefId + "']");
// if item == null -> TODO Error
}
el_items.add(el_item);
}
// --- 2. select all items from the objectbank which fulfill the selection
// criteria
Element el_selordering = (Element) el_section.selectSingleNode("selection_ordering");
if (el_selordering != null) {
// do some selection and ordering
// here comes the selection....
// xpath =
// "//item[itemmetadata/qtimetadata/qtimetadatafield[fieldlabel[text()='qmd_dificulty']
// and fieldentry[text()='4']] or
// itemmetadata/qtimetadata/qtimetadatafield[fieldlabel[text()='qmd_author']
// and fieldentry[text()='felix']]]"
//<!ELEMENT selection_ordering (qticomment? , sequence_parameter* ,
// selection* , order?)>
//<!ATTLIST selection_ordering sequence_type CDATA #IMPLIED >
//<!ELEMENT selection (sourcebank_ref? , selection_number? ,
// selection_metadata? ,
// (and_selection | or_selection | not_selection | selection_extension)?)>
//<!ELEMENT sourcebank_ref (#PCDATA)>
//not <!ELEMENT order (order_extension?)>
//<!ATTLIST order order_type CDATA #REQUIRED >
//<!ELEMENT selection_number (#PCDATA)>
//not <!ELEMENT sequence_parameter (#PCDATA)>
//not <!ATTLIST sequence_parameter %I_Pname; >
List el_selections = el_selordering.selectNodes("selection");
// iterate over all selection elements : after each we have some items to
// add to the run-time-section
for (Iterator it_selection = el_selections.iterator(); it_selection.hasNext();) {
List selectedItems;
Element el_selection = (Element) it_selection.next();
Element el_sourcebankref = (Element) el_selection.selectSingleNode("sourcebank_ref");
if (el_sourcebankref == null) {
// no reference to sourcebank, -> take internal one, but dtd disallows
// it!?? TODO
/*
* 2:27 PM] <felix.jost> aus ims qti sao: [2:27 PM] <felix.jost> 3.2.1
* <sourcebank_ref> Description: Identifies the objectbank to which
* the selection and ordering rules are to be applied. This objectbank
* may or may not be contained in the same <questestinterop> package.
* [2:27 PM] <felix.jost> aber dtd: [2:28 PM] <felix.jost> <!ELEMENT
* questestinterop (qticomment? , (objectbank | assessment | (section |
* item)+))>
*/
selectedItems = new ArrayList();
} else {
String sourceBankRef = el_sourcebankref.getText();
Element objectBank = assessInstance.getResolver().getObjectBank(sourceBankRef);
// traverse 1.: process "and" or "or" or "not" selection to get the
// items, if existing, otherwise take all items
// 2.: do the selection_number
Element andornot_selection = (Element) el_selection
.selectSingleNode("and_selection|or_selection|not_selection|selection_metadata");
StringBuilder select_expr = new StringBuilder("//item");
if (andornot_selection != null) {
// some criteria, extend above xpath to select only the appropriate
// elements
select_expr.append("[");
String elName = andornot_selection.getName();
ExpressionBuilder eb = QTIHelper.getExpressionBuilder(elName);
eb.buildXPathExpression(andornot_selection, select_expr, false, true);
select_expr.append("]");
}
selectedItems = objectBank.selectNodes(select_expr.toString());
el_items.addAll(selectedItems);
}
Element el_selection_number = (Element) el_selection.selectSingleNode("selection_number");
// --- 3. if selection_number exists, pick out some items
if (el_selection_number != null) {
String sNum = el_selection_number.getText();
int num = new Integer(sNum).intValue();
// now choose some x out of the items if selection_number exists
List newList = new ArrayList();
Random r = new Random();
int size = el_items.size();
// if num > size ??e.g. 5 elements should be picked, but there are
// only four
if (num > size) num = size;
for (int i = 0; i < num; i++) {
int n = r.nextInt(size--);
Object o = el_items.remove(n);
newList.add(o);
}
el_items = newList;
/*
* pick out items -> remove unused items from section
*/
items.removeAll(el_items);
for (Iterator iter = items.iterator(); iter.hasNext();) {
el_section.remove((Node)iter.next());
}
}
// append found items to existing ones
}
} // end of el_ordering != null
// if there is order = random -> shuffle
//<order order_type="Random"/>
if (el_selordering != null) {
Element el_order = (Element) el_selordering.selectSingleNode("order");
if (el_order != null) {
String order_type = el_order.attributeValue("order_type");
if (order_type.equals("Random")) {
Collections.shuffle(el_items);
}
}
}
// now wrap all item contexts
itemContexts = new ArrayList(10);
for (Iterator iter = el_items.iterator(); iter.hasNext();) {
Element item = (Element) iter.next();
item.detach();
ItemContext itc = new ItemContext();
itc.setUp(assessInstance, item, sw);
if (durationLimit != -1 && assessInstance.isSectionPage()) itc.clearDurationLimit();
itemContexts.add(itc);
}
//outcomesProcessing
//<!ELEMENT section (qticomment? , duration? , qtimetadata* ,
// objectives* , sectioncontrol* , sectionprecondition* ,
// sectionpostcondition* ,
// rubric* , presentation_material? ,
// outcomes_processing* , sectionproc_extension? ,
// sectionfeedback* , selection_ordering? ,
// reference? , (itemref | item | sectionref | section)*)>
//<!ELEMENT outcomes_processing (qticomment? , outcomes ,
// objects_condition* , processing_parameter* , map_output* ,
// outcomes_feedback_test*)>
//<!ELEMENT outcomes (qticomment? , (decvar , interpretvar*)+)>
//<!ELEMENT decvar (#PCDATA)>
//<!ATTLIST decvar %I_VarName; .......cutvalue CDATA #IMPLIED >
Element el_outpro = (Element) el_section.selectSingleNode("outcomes_processing");
if (el_outpro != null) {
// get the scoring model: we need it later for calculating the score
//<!ENTITY % I_ScoreModel " scoremodel CDATA #IMPLIED">
scoremodel = el_outpro.attributeValue("scoremodel");
// may be null -> then assume SumOfScores
// set the cutvalue if given (only variable score)
cutvalue = QTIHelper.getIntAttribute(el_outpro, "outcomes/decvar[@varname='SCORE']", "cutvalue");
List el_oft = el_outpro.selectNodes("outcomes_feedback_test");
if (el_oft.size() != 0) {
feedbacktesting = true;
}
}
}
/**
* Method calcFeedBack.
*/
private void calcFeedBack() {
List el_ofts = el_section.selectNodes("outcomes_processing/outcomes_feedback_test");
feedbackavailable = false;
for (Iterator it_oft = el_ofts.iterator(); it_oft.hasNext();) {
Element el_oft = (Element) it_oft.next();
//<!ELEMENT outcomes_feedback_test (test_variable , displayfeedback+)>
Element el_testvar = (Element) el_oft.selectSingleNode("test_variable");
// must exist: dtd
//<!ELEMENT test_variable (variable_test | and_test | or_test |
// not_test)>
Element el_varandornot = (Element) el_testvar.selectSingleNode("variable_test|and_test|or_test|not_test");
String elname = el_varandornot.getName();
ScoreBooleanEvaluable sbe = QTIHelper.getSectionBooleanEvaluableInstance(elname);
float totalscore = getScore();
boolean fulfilled = sbe.eval(el_varandornot, totalscore);
if (fulfilled) {
// get feedback
Element el_displayfeedback = (Element) el_oft.selectSingleNode("displayfeedback");
String linkRefId = el_displayfeedback.attributeValue("linkrefid");
// must exist (dtd)
// ignore feedbacktype, since we section or assess feedback only accepts
// material, no hints or solutions
Element el_resolved = (Element) el_section.selectSingleNode(".//sectionfeedback[@ident='" + linkRefId + "']");
getOutput().setEl_response(new SectionFeedback(el_resolved));
// give the whole sectionfeedback to render
feedbackavailable = true;
}
}
}
/**
* @return List of ItemContext instances
*/
public List getItemContextsToRender() {
return itemContexts;
}
/**
* Method calcScore.
*/
private void calcScore() {
totalScore = 0;
if (scoremodel == null || scoremodel.equalsIgnoreCase("SumOfScores")) { // sumofScores
for (Iterator iter = itemContexts.iterator(); iter.hasNext();) {
ItemContext ict = (ItemContext) iter.next();
totalScore += ict.getScore();
}
} else if (scoremodel.equalsIgnoreCase("NumberCorrect")) {
totalScore = 0;
int tmpscore = 0;
// correct number of items: an item is correct if it reaches the cutvalue
for (Iterator iter = itemContexts.iterator(); iter.hasNext();) {
ItemContext ict = (ItemContext) iter.next();
Variable var = ict.getVariables().getSCOREVariable();
if (var == null) {
// we demand that a SCORE variable must always exist
throw new RuntimeException("no SCORE def for " + ict.getIdent());
} else {
float itemscore = var.getTruncatedValue();
float itemcutval = var.getCutValue();
if (itemscore >= itemcutval) tmpscore++; // count items correct
}
}
if (tmpscore >= cutvalue) totalScore = 1.0f; // cutvalue of the section
} else {
throw new RuntimeException("scoring algorithm " + scoremodel + " not supported");
}
}
/**
* @return
*/
public float getScore() {
calcScore();
return totalScore;
}
/**
* @return
*/
public String getIdent() {
return ident;
}
/**
* @return
*/
public String getTitle() {
return el_section.attributeValue("title");
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return "<br />section:" + getIdent() + " score:" + getScore() + ",items" + itemContexts.toString() + "=" + super.toString();
}
/**
* Returns the sectionItems.
*
* @return List
*/
public List getSectionItemContexts() {
return itemContexts;
}
/**
* Method getItemContext.
*
* @param sIdent
* @return ItemContext
*/
public ItemContext getItemContext(String sIdent) {
for (Iterator it_icts = getSectionItemContexts().iterator(); it_icts.hasNext();) {
ItemContext itc = (ItemContext) it_icts.next();
if (itc.getIdent().equals(sIdent)) { return itc; }
}
// not found: for a user answer, no corresponding item could be found ->
// error (in form?,logic...)
// the other way round: no answer of user = the ItemInput of an ItemContext
// is simply not set
throw new RuntimeException("could not find an itemcontext with ident " + ident + " , but got an itemInput with this ident!");
}
/**
* @return
*/
public int getItemsAttemptedCount() {
int total = 0;
int itcnt = getItemContextCount();
for (int i = 0; i < itcnt; i++) {
if (getItemContext(i).getTimesAnswered() > 0) total++;
}
return total;
}
/**
* @return
*/
public int getItemsPresentedCount() {
int total = 0;
int itcnt = getItemContextCount();
for (int i = 0; i < itcnt; i++) {
if (getItemContext(i).isStarted()) total++;
}
return total;
}
/**
* @return
*/
public int getItemContextCount() {
return itemContexts.size();
}
/**
* Returns the currentItemContextPos.
*
* @return int
*/
public int getCurrentItemContextPos() {
return currentItemContextPos;
}
/**
* Sets the currentItemContextPos.
*
* @param currentItemContextPos The currentItemContextPos to set
*/
public void setCurrentItemContextPos(int currentItemContextPos) {
this.currentItemContextPos = currentItemContextPos;
}
/**
* Method getCurrentItemContext.
*
* @return Element
*/
public ItemContext getCurrentItemContext() {
return (ItemContext) itemContexts.get(currentItemContextPos);
}
/**
* checks whether the user may still submit answers
*
* @return
*/
public boolean isOpen() {
// not started yet or no timelimit or within timelimit
return onTime();
}
/**
* @return
*/
public boolean onTime() {
// ok if not started yet or no time limit or within limit
return (timeOfStart == -1) || (durationLimit == -1) || (System.currentTimeMillis() < (timeOfStart + durationLimit));
}
/**
* @return
*/
public boolean isStarted() {
return (timeOfStart != -1);
}
/**
* @param pos
* @return
*/
public ItemContext getItemContext(int pos) {
return (ItemContext) itemContexts.get(pos);
}
/**
* Returns the timeOfStart.
*
* @return long
*/
public long getTimeOfStart() {
return timeOfStart;
}
/**
* Returns the timeOfStop.
*
* @return long
*/
public long getLatestAnswerTime() {
return latestAnswerTime;
}
/**
* Get the time limit set on this section
*
* @return
*/
public long getDurationLimit() {
return durationLimit;
}
/**
* Get the time to completion for this section
*
* @return
*/
public long getDuration() {
if (timesAnswered == 0) return -1;
return latestAnswerTime - timeOfStart;
}
/**
* @return
*/
public float getMaxScore() {
float score = 0.0f;
for (int i = 0; i < getItemContextCount(); i++) {
ItemContext itctx = getItemContext(i);
float maxScore = itctx.getMaxScore();
if (maxScore == -1) return -1;
else score += maxScore;
}
return score;
}
/**
* @return Output
*/
public Output getOutput() {
if (output == null) {
output = new Output();
}
return output;
}
/**
* @return boolean
*/
public boolean isFeedbackavailable() {
return feedbackavailable;
}
public Objectives getObjectives() {
return objectives;
}
public int getCutValue() {
return cutvalue;
}
}