/** * 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. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. */ 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 org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.XPath; import org.olat.ims.qti.QTIConstants; import org.olat.ims.qti.container.qtielements.Item; import org.olat.ims.qti.process.AssessmentInstance; import org.olat.ims.qti.process.QTIHelper; import org.olat.ims.qti.process.elements.QTI_item; /** * @author Potable Shop * restrictions on items so far: - only one resprocessing element is * evaluated. duration: from ims qti 1.2.1 best practice: duration The * duration element is used within the item, section and assessment * elements to define the permitted duration for the enclosed activity. * The duration is defined as the period between the activity's * activation and completion. The ISO 8601 format is used: * PnYnMnDTnHnMnS. in mchc_smimr_104.xml example: 0000:00:00T:01:00:00d * in ...101.xml: pTH02 iso website: * http://www.iso.org/iso/en/prods-services/popstds/datesandtime.html * and http://www.w3.org/TR/NOTE-datetime from some page: specified in * numbers of years [Y], months [M] or weeks [W], days [D], hours [H] * and so on, introduced by a ?P?, for example: P6W2D -> not a date, but * only a duration makes sense: -> I shall take the ISO 8601 Time of the * day hh:mm:ss format for now */ public class ItemContext implements Serializable { private AssessmentInstance assessInstance; // TODO: no, complicated, since in several objectbanks.... readonly ref!: the // ref to the el_item; transient since it we don't want to serialize it (too // long) and can reattach it later private Element el_shuffled_item; // Internal representation of an item. private Item qtiItem; // the outcome of a item after it was evaluated. // it contains variables with values private Variables variables; // contains the output after an evaluation like hint, solution, feedback private Output output; // the last answer of the user concerning this question private ItemInput itemInput; // number of times this question has been answered private int timesAnswered; private long timeOfStart; // server time (in miliseconds) at the time of the start of the item private long latestAnswerTime; // server time (in miliseconds) at the time of the latest answering of the // item // the ident of the item - needed to reattach the correct element of the // qti-tree // after a deserialize in case of a crash. private String ident; // -- the following are also in the qti tree, but are here for convenience --- // max number of attempts the user may try this question private int maxAttempts; // -1 for infinite private long durationLimit; // private boolean evalNeeded; private boolean feedback = true; // flags private boolean hints = true; private boolean solutions = true; private int hintLevel; // /** * default constructor needed for persistence */ public ItemContext() { // } /** * */ public void init() { ident = el_shuffled_item.attributeValue("ident"); resetVariables(); itemInput = null; timesAnswered = 0; evalNeeded = false; output = new Output(); timeOfStart = -1; // not started yet latestAnswerTime = -1; // not yet answered hintLevel = 0; // no hint with type "Incremental" has been given so far Element dur = (Element) el_shuffled_item.selectSingleNode("duration"); if (dur == null) { durationLimit = -1; // no limit } else { String sdur = dur.getText(); durationLimit = QTIHelper.parseISODuration(sdur); } String strmaxattempts = el_shuffled_item.attributeValue("maxattempts"); if (strmaxattempts == null) { maxAttempts = -1; // no limit } else { maxAttempts = Integer.parseInt(strmaxattempts); } } /** * Method setUp. * * @param assessInstance * @param el_orig_item * @param sw */ public void setUp(AssessmentInstance assessInstance, Element el_orig_item, Switches sw) { this.assessInstance = assessInstance; this.el_shuffled_item = shuffle(el_orig_item); this.qtiItem = new Item(el_shuffled_item); if (sw == null) { // no section switches dominate, take item switches // retrieve item switches Element el_control = (Element) el_orig_item.selectSingleNode("itemcontrol"); if (el_control != null) { String feedbackswitch = el_control.attributeValue("feedbackswitch"); String hintswitch = el_control.attributeValue("hintswitch"); String solutionswitch = el_control.attributeValue("solutionswitch"); boolean newFeedback = (feedbackswitch == null) ? true : feedbackswitch.equals("Yes"); boolean newHints = (hintswitch == null) ? true : hintswitch.equals("Yes"); boolean newSolutions = (solutionswitch == null) ? true : solutionswitch.equals("Yes"); sw = new Switches(newFeedback, newHints, newSolutions); } } this.feedback = (sw != null ? sw.isFeedback() : true); this.solutions = (sw != null ? sw.isSolutions() : true); this.hints = (sw != null ? sw.isHints() : true); init(); } /** * Method shuffle. shuffle clones the current item (since the whole qti tree * is readonly) and shuffles it * * @param item * @return Element */ private Element shuffle(Element item) { // get the render_choice XPath choice = DocumentHelper.createXPath(".//render_choice[@shuffle=\"Yes\"]"); Element tel_rendchoice = (Element) choice.selectSingleNode(item); //if shuffle is disable, just return the item if (tel_rendchoice == null) return item; // else: we have to shuffle // assume: all response_label have same parent: either render_choice or a // flow_label Element shuffleItem = item.createCopy(); // clone the whole item Element el_rendchoice = (Element) choice.selectSingleNode(shuffleItem); // <!ELEMENT render_choice ((material | material_ref | response_label | // flow_label)* ,response_na?)> // <!ATTLIST response_label rshuffle (Yes | No ) 'Yes' ..... List el_labels = el_rendchoice.selectNodes(".//response_label[@rshuffle=\"Yes\"]"); int shusize = el_labels.size(); // set up a list of children with their parents and the position of the // child (in case several children have the same parent List<Element> respList = new ArrayList<>(shusize); List<Element> parentList = new ArrayList<>(shusize); int[] posList = new int[shusize]; int j = 0; for (Iterator responses = el_labels.iterator(); responses.hasNext();) { Element response = (Element) responses.next(); Element parent = response.getParent(); int pos = parent.indexOf(response); posList[j++] = pos; respList.add((Element)response.clone()); // need to use clones so they are not // attached anymore parentList.add(parent); } Collections.shuffle(respList); // put the children back to the parents for (int i = 0; i < parentList.size(); i++) { Element parent = parentList.get(i); int pos = posList[i]; Element child = respList.get(i); parent.elements().set(pos, child); } return shuffleItem; } /** * @return Variables */ public Variables getVariables() { return variables; } /** * @return */ public Output getOutput() { return output; } /** * @return ItemInput */ public ItemInput getItemInput() { return itemInput; } /** * Sets the itemInput. * @param theItemInput The itemInput to set. max be null for "unanswered" * @return the status of the add operation, e.g. ok */ public int addItemInput(ItemInput theItemInput) { boolean underMax = isUnderMaxAttempts(); boolean onTime = isOnTime(); if (underMax && onTime) { start(); timesAnswered++; latestAnswerTime = System.currentTimeMillis(); this.itemInput = theItemInput; evalNeeded = true; return QTIConstants.ITEM_SUBMITTED; } else { if (!underMax) return QTIConstants.ERROR_SUBMITTEDITEM_TOOMANYATTEMPTS; else return QTIConstants.ERROR_SUBMITTEDITEM_OUTOFTIME; } } /** * @return int */ public int getTimesAnswered() { return timesAnswered; } /** * Method getIdent. * * @return String */ public String getIdent() { return ident; } /** * @see java.lang.Object#toString() */ public String toString() { return "item:" + getIdent() + ":inp:" + itemInput + ",vars:" + variables + ",out:" + output + "=" + super.toString(); } /** * Returns the timeOfStart. * * @return long */ public long getTimeOfStart() { return timeOfStart; } /** * @return */ public boolean isOpen() { // open when in timelimit or not started yet or no timelimit AND maxattempts // undef or attempts < maxattempts boolean ok = isOnTime() && isUnderMaxAttempts(); return ok; } /** * @return */ public boolean isOnTime() { return (timeOfStart == -1) || (durationLimit == -1) || (System.currentTimeMillis() < timeOfStart + durationLimit); } /** * @return */ public boolean isUnderMaxAttempts() { return (maxAttempts == -1 || timesAnswered < maxAttempts); } /** * @return Element */ public Element getEl_item() { return el_shuffled_item; } /** * Method start. */ public void start() { if (timeOfStart == -1) { // if not started already timeOfStart = System.currentTimeMillis(); } } /** * return duration for answered items * @return */ public long getTimeSpent() { if (timesAnswered == 0) return -1; return latestAnswerTime - timeOfStart; } /** * Returns the duration. * * @return long */ public long getDurationLimit() { return durationLimit; } /** * @return */ public boolean isStarted() { return (timeOfStart != -1); } /** * @return int */ public int getMaxAttempts() { return maxAttempts; } /** * @return long */ public long getLatestAnswerTime() { return latestAnswerTime; } /** * */ public void eval() { if (assessInstance.isSurvey()) return; if (evalNeeded) { QTI_item qtiItem = QTIHelper.getQtiItem(); output = new Output(); // clear any previous feedback, hints, solutions qtiItem.evalAnswer(this); evalNeeded = false; } } /** * @return */ public float getMaxScore() { Variable var = getVariables().getSCOREVariable(); return var.getMaxValue(); } /** * Returns the value of the SCORE variable * * @return */ public float getScore() { Variable var = getVariables().getSCOREVariable(); if (var == null) { if(ident.startsWith("QTIEDIT:ESSAY")) { return 0.0f; } // we demand that a SCORE variable must always exist throw new RuntimeException("no SCORE def for " + getIdent()); } else { float sc = var.getTruncatedValue(); return sc; } } /** * @return boolean */ public boolean isFeedback() { return feedback; } /** * @return boolean */ public boolean isHints() { return hints; } /** * @return boolean */ public boolean isSolutions() { return solutions; } /** * */ public void resetVariables() { Element el_outcomes = (Element) getEl_item().selectSingleNode("resprocessing/outcomes"); variables = QTIHelper.declareVariables(el_outcomes); } /** * @return int */ public int getHintLevel() { return hintLevel; } /** * Sets the hintLevel. * * @param hintLevel The hintLevel to set */ public void setHintLevel(int hintLevel) { this.hintLevel = hintLevel; } /** * */ public void clearDurationLimit() { durationLimit = -1; } /** * @return item */ public Item getQtiItem() { return qtiItem; } }