/** * 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 java.util.Random; import org.dom4j.Document; import org.dom4j.Element; import org.olat.ims.qti.container.qtielements.AssessFeedback; import org.olat.ims.qti.container.qtielements.Objectives; 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; /** * contains the sections of the assignment. assumption: each toplevel-section of * an assignment means one screen <!ELEMENT assessment (qticomment? , duration? , * qtimetadata* , objectives* , assessmentcontrol* , rubric* , * presentation_material? , outcomes_processing* , assessproc_extension? , * assessfeedback* , selection_ordering? , reference? , (sectionref | * section)+)> <!ATTLIST assessment %I_Ident; %I_Title; xml:lang CDATA #IMPLIED > * * @author Felix Jost */ public class AssessmentContext implements Serializable { // readonly ref!: the ref to the el_assessment; transient since it we don't // want to serialize it (too long) and can reattach it later //private transient Element el_assessment; private String ident; private String title; private AssessmentInstance assessInstance; private Element el_assessment; private Objectives objectives; private Switches switches = null; private Output output; // the sectioncontexts of this assessment private List<SectionContext> sectionContexts; // the current section beeing chosen by the user or forced by the system private int currentSectionContextPos; private long timeOfStart; // server time at the time of the start of the assessment private long timeOfStop; // server time at the time of the start of the assessment private long durationLimit; // private float cutvalue = -1.0f; private String scoremodel; private boolean feedbacktesting; private boolean feedbackavailable; /** * default constructor needed for persistence */ public AssessmentContext() { // } /** * */ public void init() { currentSectionContextPos = -1; feedbacktesting = false; feedbackavailable = false; timeOfStart = -1; // not started yet timeOfStop = -1; // not stopped yet } /** * Method setUp. * * @param assessInstance */ public void setUp(AssessmentInstance assessInstance) { this.assessInstance = assessInstance; init(); Document el_questestinterop = assessInstance.getResolver().getQTIDocument(); el_assessment = (Element) el_questestinterop.selectSingleNode("questestinterop/assessment"); ident = el_assessment.attributeValue("ident"); title = el_assessment.attributeValue("title"); Element dur = (Element) el_assessment.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_assessment.selectSingleNode("objectives"); if (el_objectives != null) objectives = new Objectives(el_objectives); // set feedback, hint, and solutions switches //<!ENTITY % I_FeedbackSwitch " feedbackswitch (Yes | No ) 'Yes'"> //<!ENTITY % I_HintSwitch " hintswitch (Yes | No ) 'Yes'"> //<!ENTITY % I_SolutionSwitch " solutionswitch (Yes | No ) 'Yes'"> //<!ELEMENT assessment (qticomment? , duration? , qtimetadata* , // objectives* , assessmentcontrol* , rubric* , presentation_material? , // outcomes_processing* , assessproc_extension? , assessfeedback* , // selection_ordering? , reference? , (sectionref | section)+)> //<!ELEMENT assessmentcontrol (qticomment?)> Element el_control = (Element) el_assessment.selectSingleNode("assessmentcontrol"); if (el_control != null) { String feedbackswitch = el_control.attributeValue("feedbackswitch"); String hintswitch = el_control.attributeValue("hintswitch"); String solutionswitch = el_control.attributeValue("solutionswitch"); boolean feedback = (feedbackswitch == null) ? true : feedbackswitch.equals("Yes"); boolean hints = (hintswitch == null) ? true : hintswitch.equals("Yes"); boolean solutions = (solutionswitch == null) ? true : solutionswitch.equals("Yes"); switches = new Switches(feedback, hints, solutions); } // scoring model and outcomes processing Element el_outpro = (Element) el_assessment.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.getFloatAttribute(el_outpro, "outcomes/decvar[@varname='SCORE']", "cutvalue"); List el_oft = el_outpro.selectNodes("outcomes_feedback_test"); if (el_oft.size() != 0) { feedbacktesting = true; } } initSections(el_assessment, switches); init(); } private void initSections(Element assessment, Switches sw) { sectionContexts = new ArrayList<SectionContext>(2); List<Element> el_sections = new ArrayList<>(); //<!ELEMENT sectionref (#PCDATA)> //<!ATTLIST sectionref %I_LinkRefId; > List sections = assessment.selectNodes("section|sectionref"); for (Iterator iter = sections.iterator(); iter.hasNext();) { Element el_section = (Element) iter.next(); // resolve sectionref into the correct sections if (el_section.getName().equals("sectionref")) { String linkRefId = el_section.attributeValue("linkrefid"); el_section = (Element) el_section.selectSingleNode("//section[@ident='" + linkRefId + "']"); if (el_section == null) { throw new RuntimeException("sectionref with ref '" + linkRefId + "' could not be resolved"); } } el_sections.add(el_section); } Element el_selordering = (Element) assessment.selectSingleNode("selection_ordering"); if (el_selordering != null) { // do some selection and ordering //<!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 selectedSections; 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)+))> */ selectedSections = 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("//section"); 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("]"); } selectedSections = objectBank.selectNodes(select_expr.toString()); el_sections.addAll(selectedSections); } 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<Element> newList = new ArrayList<Element>(); Random r = new Random(); int size = el_sections.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--); Element o = el_sections.remove(n); newList.add(o); } el_sections = newList; /* * pick out items -> remove unused items from section */ sections.removeAll(el_sections); for (Iterator iter = sections.iterator(); iter.hasNext();) { el_sections.remove(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_sections); } } } for (Iterator<Element> iter = el_sections.iterator(); iter.hasNext();) { Element section = iter.next(); SectionContext sc = new SectionContext(); sc.setUp(assessInstance, section, sw); sectionContexts.add(sc); } } /** * start assessment */ public void start() { // if not started yet, start if (timeOfStart == -1) { timeOfStart = System.currentTimeMillis(); } } /** * stop assessment */ public void stop() { if (timeOfStart != -1 && timeOfStop == -1) { timeOfStop = System.currentTimeMillis(); } if (getCurrentSectionContext() != null) getCurrentSectionContext().sectionWasSubmitted(); } /** * */ public void eval() { if (assessInstance.isSurvey()) return; int sccnt = getSectionContextCount(); for (int i = 0; i < sccnt; i++) { SectionContext sc = getSectionContext(i); sc.eval(); } if (feedbacktesting) calcFeedBack(); } /** * @see java.lang.Object#toString() */ public String toString() { return "<br /><br />assessment:" + sectionContexts.toString() + "=" + super.toString(); } /** * Method getIdent. * * @return String */ public String getIdent() { return ident; } /** * @return */ public String getTitle() { return title; } /** * @return */ public SectionContext getCurrentSectionContext() { if (currentSectionContextPos == -1) return null; SectionContext sc = sectionContexts.get(currentSectionContextPos); return sc; } /** * Sets the currentSectionPos. * * @param currentSectionPos The currentSectionPos to set */ public void setCurrentSectionPos(int currentSectionPos) { if (currentSectionPos >= sectionContexts.size()) { throw new RuntimeException("error"); } this.currentSectionContextPos = currentSectionPos; } /** * Method getSectionContextCount. * * @return int */ public int getSectionContextCount() { return sectionContexts.size(); } /** * Return the total items in all sections of the assessment. * @return Total number of items */ public int getItemContextCount() { int count = 0; int sccnt = getSectionContextCount(); for (int i = 0; i < sccnt; i++) { SectionContext sc = getSectionContext(i); count += sc.getItemContextCount(); } return count; } /** * Get the position of the current item within the assessment. * @return position of the current item within the assessment. */ public int getItemPosWithinAssessment() { if (currentSectionContextPos == -1) return 1; // first question int currentPos = 1; for (int i=0; i < getCurrentSectionContextPos(); i++) { // count all items in previous section currentPos += getSectionContext(i).getItemContextCount(); } SectionContext curSectionContext = getCurrentSectionContext(); if (curSectionContext.getCurrentItemContextPos() != -1) // this is a section page, just add 1 item to the current pos currentPos += curSectionContext.getCurrentItemContextPos(); return currentPos; } /** * Get the position of the first item within the assessment. * @return position of the first item within the assessment. */ public int getFirstItemPosWithinSection() { if (currentSectionContextPos == -1) return 1; // first question int currentPos = 1; for (int i=0; i < getCurrentSectionContextPos(); i++) { // count all items in previous section currentPos += getSectionContext(i).getItemContextCount(); } return currentPos; } /** * Get the position of the last item of the current section within the assessment. * @return position of the last item of the current section within the assessment. */ public int getLastItemPosWithinSection() { int currentPos = 0; for (int i=0;getCurrentSectionContextPos() > -1 && i < getCurrentSectionContextPos(); i++) { // count all items in previous section currentPos += getSectionContext(i).getItemContextCount(); } getCurrentSectionContext(); if (getCurrentSectionContextPos()>-1) { currentPos += getSectionContext(getCurrentSectionContextPos()).getItemContextCount(); } return currentPos; } /** * Method setCurrentSectionContextPos. * * @param i */ public void setCurrentSectionContextPos(int i) { currentSectionContextPos = i; } /** * Returns the currentSectionContextPos. * * @return int */ public int getCurrentSectionContextPos() { return currentSectionContextPos; } /** * checks whether the user may still submit answers * * @return */ public boolean isOpen() { // not started yet or no timelimit or within timelimit return (timeOfStart == -1) || (durationLimit == -1) || (System.currentTimeMillis() < (timeOfStart + durationLimit)); } /** * @return */ public boolean isStarted() { return (timeOfStart != -1); } /** * @param pos * @return */ public SectionContext getSectionContext(int pos) { return sectionContexts.get(pos); } /** * @return long */ public long getDurationLimit() { return durationLimit; } /** * Return the time to completion for this assessment * * @return long Millis to completion */ public long getDuration() { if (timeOfStart == -1 | timeOfStop == -1) return 0; return timeOfStop - timeOfStart; } /** * Get the maximum score for this assessment. (Sum of maxscore of all items) * * @return */ public float getMaxScore() { float count = 0.0f; for (Iterator<SectionContext> iter = sectionContexts.iterator(); iter.hasNext();) { SectionContext sc = iter.next(); float maxScore = sc.getMaxScore(); if (maxScore == -1) return -1; else count += maxScore; } return count; } /** * @return */ public float getScore() { if (scoremodel == null || scoremodel.equalsIgnoreCase("SumOfScores")) { // sumofScores float count = 0; for (Iterator<SectionContext> iter = sectionContexts.iterator(); iter.hasNext();) { SectionContext sc = iter.next(); count += sc.getScore(); } return count; } else if (scoremodel.equalsIgnoreCase("NumberCorrect")) { float tmpscore = 0.0f; // calculate correct number of sections: an section is correct if its // correct items reach the section's cutvalue for (Iterator<SectionContext> iter = sectionContexts.iterator(); iter.hasNext();) { SectionContext sc = iter.next(); float sscore = sc.getScore(); if (sscore >= cutvalue) tmpscore++; // count items correct } return tmpscore; } else { throw new RuntimeException("scoring algorithm " + scoremodel + " not supported"); } } /** * @return */ public boolean isPassed() { float score = getScore(); return (score >= cutvalue); } /** * @return */ public int getItemsPresentedCount() { int count = 0; for (Iterator<SectionContext> iter = sectionContexts.iterator(); iter.hasNext();) { SectionContext sc = iter.next(); count += sc.getItemsPresentedCount(); } return count; } /** * @return */ public int getItemsAnsweredCount() { int count = 0; for (Iterator<SectionContext> iter = sectionContexts.iterator(); iter.hasNext();) { SectionContext sc = iter.next(); count += sc.getItemsAnsweredCount(); } return count; } /** * @return */ public int getItemsAttemptedCount() { int count = 0; for (Iterator<SectionContext> iter = sectionContexts.iterator(); iter.hasNext();) { SectionContext sc = iter.next(); count += sc.getItemsAttemptedCount(); } return count; } /** * Method calcFeedBack. */ private void calcFeedBack() { if (feedbacktesting) { List el_ofts = el_assessment.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_assessment.selectSingleNode(".//assessfeedback[@ident='" + linkRefId + "']"); getOutput().setEl_response(new AssessFeedback(el_resolved)); // give the whole assessmentfeedback to render feedbackavailable = true; } } } } /** * @return Output */ public Output getOutput() { if (output == null) { output = new Output(); } return output; } /** * @return */ public Switches getSwitches() { return switches; } /** * @param switches */ public void setSwitches(Switches switches) { this.switches = switches; } /** * @return */ public boolean isFeedbackavailable() { return feedbackavailable; } /** * @param b */ public void setFeedbackavailable(boolean b) { feedbackavailable = b; } /** * @return float */ public float getCutvalue() { return cutvalue; } /** * @return */ public long getTimeOfStart() { return timeOfStart; } /** * @return */ public long getTimeOfStop() { return timeOfStop; } public Objectives getObjectives() { return objectives; } }