2 /****************************************************************/
4 /****************************************************************/
5 /* Copyright (c) 2002-2008 by Greg Gay, Cindy Qi Li, */
7 /* Adaptive Technology Resource Centre / University of Toronto */
10 /* This program is free software. You can redistribute it and/or*/
11 /* modify it under the terms of the GNU General Public License */
12 /* as published by the Free Software Foundation. */
13 /****************************************************************/
17 define('AT_QTI_REPONSE_GRP', 1);
18 define('AT_QTI_REPONSE_LID', 2);
19 define('AT_QTI_REPONSE_STR', 3);
23 * Class for parsing XML language info and returning a QTI Object
29 var $parser; // the XML handler
30 var $character_data; // tmp variable for storing the data
31 var $element_path; // array of element paths (basically a stack)
32 var $title; //title for this question test
33 var $q_identifiers = array(); //The identifier of the choice. This identifier must not be used by any other choice or item variable.
34 var $question = ''; //question of this QTI
35 var $response_type = array(); //detects what type of question this would be.
36 var $relative_path = ''; //the relative path to all resources in this xml.
39 var $choices = array(); //answers array that keep tracks of all the correct answers
40 var $groups = array(); //groups for matching, the left handside to match with the different choices
41 var $attributes = array(); //tag attribute
42 var $answers = array(); //correct answers
43 var $response_label = array(); //temporary holders for response labels
44 var $field_label = array(); //fields label
45 var $field_entry = array(); //fields entry
46 var $feedback = array(); //question feedback
47 var $item_num = 0; //item number
48 var $items = array(); //stacks of media items, ie. img, embed, ahref etc.
49 var $qmd_itemtype = -1; //qmd flag
50 var $temp_answer = array(); //store the temp answer stack
51 var $answers_for_matching = array();
52 var $weights = array(); //the weight of each question
54 function QTIParser() {
55 $this->parser = xml_parser_create();
57 xml_set_object($this->parser, $this);
58 xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, false); /* conform to W3C specs */
59 xml_set_element_handler($this->parser, 'startElement', 'endElement');
60 xml_set_character_data_handler($this->parser, 'characterData');
64 // @return true if parsed successfully, false otherwise
65 function parse($xml_data) {
66 $this->element_path = array();
67 $this->character_data = '';
68 xml_parse($this->parser, $xml_data, TRUE);
70 //Loop thru each item and replace if existed
71 foreach ($this->answers_for_matching as $afm_k => $afk_v){
72 if (!empty($this->answers_for_matching[$afm_k])){
73 $this->answers[$afm_k] = $afk_v;
77 if(in_array('questestinterop', $this->element_path) ||
78 in_array('assessment', $this->element_path)){
79 //this is a v2.1+ package
87 function startElement($parser, $name, $attributes) {
88 // debug($attributes, $name );
92 $this->title = $attributes['title'];
95 if ($this->response_type[$this->item_num] <= 0) {
96 $this->response_type[$this->item_num] = AT_QTI_REPONSE_LID;
99 if ($this->response_type[$this->item_num] <= 0) {
100 $this->response_type[$this->item_num] = AT_QTI_REPONSE_GRP;
103 $this->attributes[$this->item_num][$name]['ident'] = $attributes['ident'];
104 $this->attributes[$this->item_num][$name]['rcardinality'] = $attributes['rcardinality'];
105 if ($this->response_type[$this->item_num] <= 0) {
106 $this->response_type[$this->item_num] = AT_QTI_REPONSE_STR;
109 case 'response_label':
110 if(!isset($this->choices[$this->item_num][$attributes['ident']])){
111 if (!is_array($this->response_label[$this->item_num])){
112 $this->response_label[$this->item_num] = array();
114 array_push($this->response_label[$this->item_num], $attributes['ident']);
118 $this->attributes[$this->item_num][$name]['respident'] = $attributes['respident'];
121 $this->attributes[$this->item_num][$name]['varname'] = $attributes['varname'];
123 case 'render_choice':
124 $this->attributes[$this->item_num][$name]['shuffle'] = $attributes['shuffle'];
125 $this->attributes[$this->item_num][$name]['minnumber'] = $attributes['minnumber'];
126 $this->attributes[$this->item_num][$name]['maxnumber'] = $attributes['maxnumber'];
129 $rows = intval($attributes['rows']);
132 //1,2,3,4 according to tools/tests/create_question_long.php
135 } elseif ($rows > 1 && $rows <= 5){
137 } elseif ($rows > 5){
140 $this->attributes[$this->item_num][$name]['property'] = $property;
143 $this->attributes[$this->item_num][$name]['imagtype'] = $attributes['imagtype'];
144 $this->attributes[$this->item_num][$name]['uri'] = $attributes['uri'];
147 $this->attributes[$this->item_num][$name]['audiotype'] = $attributes['audiotype'];
148 $this->attributes[$this->item_num][$name]['uri'] = $attributes['uri'];
151 $this->attributes[$this->item_num][$name]['videotype'] = $attributes['videotype'];
152 $this->attributes[$this->item_num][$name]['uri'] = $attributes['uri'];
155 $this->attributes[$this->item_num][$name]['uri'] = $attributes['uri'];
156 $this->attributes[$this->item_num][$name]['width'] = intval($attributes['width']);
157 $this->attributes[$this->item_num][$name]['height'] = intval($attributes['height']);
160 $this->attributes[$this->item_num][$name]['varname'] = $attributes['varname'];
161 $this->attributes[$this->item_num][$name]['action'] = $attributes['action'];
164 array_push($this->element_path, $name);
168 /* called when an element ends */
169 /* removed the current element from the $path */
170 function endElement($parser, $name) {
172 $current_pos = count($this->element_path) - 1;
173 $last_element = $this->element_path[$current_pos - 1];
180 $this->mat_content[$this->item_num] .= $this->reconstructRelativePath($this->character_data);
183 $this->mat_content[$this->item_num] .= '<img src="'.$this->attributes[$this->item_num][$name]['uri'].'" alt="Image Not loaded:'.$this->attributes[$this->item_num][$name]['uri'].'" />';
186 $this->mat_content[$this->item_num] .= '<embed SRC="'.$this->attributes[$this->item_num][$name]['uri'].'" autostart="false" width="145" height="60"><noembed><bgsound src="'.$this->attributes[$this->item_num][$name]['uri'].'"></noembed></embed>';
189 if ($this->attributes[$this->item_num][$name]['videotype'] == 'type/swf'){
190 $this->mat_content[$this->item_num] .= '<object type="application/x-shockwave-flash" data="' . $this->attributes[$this->item_num][$name]['uri'] . '" width="550" height="400"><param name="movie" value="'. $this->attributes[$this->item_num][$name]['uri'] .'" /></object>';
191 } elseif ($this->attributes[$this->item_num][$name]['videotype'] == 'type/mov'){
192 $this->mat_content[$this->item_num] .= '<object classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" width="550" height="400" codebase="http://www.apple.com/qtactivex/qtplugin.cab"><param name="src" value="'. $this->attributes[$this->item_num][$name]['uri'] . '" /><param name="autoplay" value="true" /><param name="controller" value="true" /><embed src="' . $this->attributes[$this->item_num][$name]['uri'] .'" width="550" height="400" controller="true" pluginspage="http://www.apple.com/quicktime/download/"></embed></object>';
196 (($this->attributes[$this->item_num][$name]['width'] != 0)? $width = $this->attributes[$this->item_num][$name]['width'] : $width = 460);
197 (($this->attributes[$this->item_num][$name]['height'] != 0)? $height = $this->attributes[$this->item_num][$name]['height'] : $height = 160);
198 $this->mat_content[$this->item_num] .= '<applet code="'.$this->attributes[$this->item_num][$name]['uri'].'" width="'.$width.'" height="'.$height.'" alt="Applet not loaded."></applet>';
201 //check who is mattext's ancestor, started from the most known inner layer
202 if (in_array('response_label', $this->element_path)){
203 if(!in_array($this->mat_content, $this->choices)){
204 //This is one of the choices.
205 if (!empty($this->response_label[$this->item_num])){
206 $this->choices[$this->item_num][array_pop($this->response_label[$this->item_num])] = $this->mat_content[$this->item_num];
209 } elseif (in_array('response_grp', $this->element_path) || in_array('response_lid', $this->element_path)){
210 //for matching, where there are groups
211 //keep in mind that Respondus handles this by using response_lid
212 $this->groups[$this->item_num][] = $this->reconstructRelativePath($this->mat_content[$this->item_num]);
213 // debug($this->character_data, 'harris - groups');
214 } elseif (in_array('presentation', $this->element_path)){
215 $this->question[$this->item_num] = $this->reconstructRelativePath($this->mat_content[$this->item_num]);
216 } elseif (in_array('itemfeedback', $this->element_path)){
217 $this->feedback[$this->item_num] = $this->mat_content[$this->item_num];
219 //once material is closed, reset the mat_content variable.
220 $this->mat_content[$this->item_num] = '';
223 //stores the answers (either correct or incorrect) into a stack
224 $this->temp_answer[$this->attributes[$this->item_num][$name]['respident']]['name'][] = $this->character_data;
225 //responses handling, remember to save the answers or match them up
226 if (!is_array($this->answers[$this->item_num])){
227 $this->answers[$this->item_num] = array();
229 array_push($this->answers[$this->item_num], $this->reconstructRelativePath($this->character_data));
232 $this->temp_answer[$this->attributes[$this->item_num]['varequal']['respident']]['value'][] = $this->character_data;
233 $this->temp_answer[$this->attributes[$this->item_num]['varequal']['respident']]['attribute'][] = $this->attributes[$this->item_num]['setvar']['varname'];
235 case 'respcondition':
236 if (empty($this->temp_answer)) {
240 //closing this tag means a selection of choices have ended. Assign the correct answer in this case.
241 $tv = $this->temp_answer[$this->attributes[$this->item_num]['varequal']['respident']];
242 // debug($tv, 'harris'.$this->item_num);
243 // debug($this->answers_for_matching[$this->item_num], 'answers');
245 //If matching, then attribute = 'Respondus_correct'; otherwise it is 'que_score'
246 if ($this->getQuestionType($this->item_num) == 5){
247 if ($tv['answerAdded']!=true){
248 foreach ($tv['attribute'] as $att_id => $att_value){
249 //Handles Respondus' (and blakcboard, angels, etc) responses schemas
250 if (strtolower($att_value)=='respondus_correct'){
251 //Then this is the right answer
252 if (!is_array($this->answers_for_matching[$this->item_num])){
253 $this->answers_for_matching[$this->item_num] = array();
255 //The condiction here is to check rather the answers have been duplicated, otherwise the indexing won't be right.
256 //sizeof[answers] != sizeof[questions], then the index matching is wrong.
257 //Created a problem though, which is then many-to-1 matching fails, cuz answers will be repeated.
258 //Sep 2,08, Fixed by adding a flag into the array
259 // if (!in_array($tv['name'][$att_id], $this->answers_for_matching[$this->item_num])){
260 array_push($this->answers_for_matching[$this->item_num], $tv['name'][$att_id]);
261 $this->temp_answer[$this->attributes[$this->item_num]['varequal']['respident']]['answerAdded'] = true;
264 $this->weights[$this->item_num] = floatval($tv['value'][$att_id]);
271 $pos = sizeof($tv['value']) - 1; //position of the last entry of the "temp answer's value" array
272 //Retrieve the last entry of the "temp answer's value" array
273 $current_answer = $tv['value'][$pos];
274 if (floatval($current_answer) > 0){
275 if (!is_array($this->answers_for_matching[$this->item_num])){
276 $this->answers_for_matching[$this->item_num] = array();
278 // if (!in_array($tv['name'][$val_id], $this->answers_for_matching[$this->item_num])){
279 array_push($this->answers_for_matching[$this->item_num], $tv['name'][$pos]);
282 $this->weights[$this->item_num] += floatval($current_answer);
288 // save this variable
289 $this->field_label[$this->item_num] = $this->character_data;
292 $this->field_entry[$this->item_num][$this->field_label[$this->item_num]] = $this->character_data;
295 //Deprecated as of QTI 1.2.
296 if (empty($this->field_entry[$this->item_num][$name])){
297 $this->field_entry[$this->item_num][$name] = $this->character_data;
303 // debug($this->element_path, "Ele Path");
305 //pop stack and reset character data, o/w it will stack up
306 array_pop($this->element_path);
307 $this->character_data = '';
311 function characterData($parser, $data){
313 if (trim($data)!=''){
314 $this->character_data .= $addslashes(preg_replace('/[\t\0\x0B(\r\n)]*/', '', $data));
315 // $this->character_data .= trim($data);
320 * This function returns the question type of this XML.
322 * @param the item_num
323 * @return 1-8, in the order of m/c, t/f, open eneded, likert, s match, order, m/a, g match
326 function getQuestionType($item_num){
327 switch ($this->field_entry[$item_num]['qmd_questiontype']){
328 case 'Multiple-choice':
330 //likert have no answers
331 if (empty($this->answers)){
342 case 'Multiple-response':
347 switch ($this->field_entry[$item_num]['qmd_itemtype']){
354 //Check if this is an ordering, or matching
356 switch ($this->response_type[$item_num]){
357 case AT_QTI_REPONSE_LID:
358 $response_obj = $this->attributes[$item_num]['response_lid'];
360 case AT_QTI_REPONSE_GRP:
361 $response_obj = $this->attributes[$item_num]['response_grp'];
363 case AT_QTI_REPONSE_STR:
364 $response_obj = $this->attributes[$item_num]['response_str'];
365 return 3; //no need to parse the rcardinality?
368 if ($response_obj['rcardinality'] == 'Ordered'){
370 } elseif ($response_obj['rcardinality'] == 'Multiple'){
371 //TODO Multiple answers, Simple matching and Graphical matching
372 if (empty($this->field_entry[$item_num])){
376 } elseif ($response_obj['rcardinality'] == 'Single'){
377 return 1; //assume mc
386 //must be used before calling parse. Otherwise it will be null.
388 function setRelativePath($path){
390 if ($path[-1] != '/'){
393 $this->relative_path = $path;
399 //when importing, the path of the images are all changed. Have to parse them out and add the extra path in.
400 //No longer needed to reconstruct, just needed to save the path, as of Aug 25th, 08. Decided to overwrite files if the same name exist.
401 function reconstructRelativePath($path){
402 //match img tag, all.
403 // if (preg_match_all('/\<img(\s[^\>])*\ssrc\=[\\\\]?\"([^\\\\^\"]+)[\\\\]?\".*\/?\>/i', $path, $matches) > 0){
404 //fixes multiple image tags within a $path
405 if (preg_match_all('/\<img(\s[\w^img]+\=[\\\\]?\"[^\\\\^\"]+[\\\\]?\")*\ssrc\=[\\\\]?\"([^\\\\^\"]+)[\\\\]?\"/i', $path, $matches) > 0){
406 foreach ($matches[2] as $k=>$v){
407 if(strpos($v, 'http://')===false && !in_array($v, $this->items)) {
408 $this->items[] = $v; //save the url of this media.
409 // $path = str_replace($v, $this->relative_path.$v, $path);
413 } elseif (preg_match_all('/\<embed(\s[^\>])*\ssrc\=[\\\\]?\"([^\\\\^\"]+)[\\\\]?\".*/i', $path, $matches) > 0){
414 foreach ($matches[2] as $k=>$v){
415 if(strpos($v, 'http://')===false && !in_array($v, $this->items)) {
416 $this->items[] = $v; //save the url of this media.
417 // $path = str_replace($v, $this->relative_path.$v, $path);
429 //Free the XML parser
430 unset($this->response_label);
431 unset($this->field_label);
432 unset($this->temp_answer);
433 xml_parser_free($this->parser);