remove old readme
[atutor.git] / mods / _core / imsqti / classes / QTIImport.class.php
1 <?php
2 /****************************************************************/
3 /* ATutor                                                                                                               */
4 /****************************************************************/
5 /* Copyright (c) 2002-2009                                                                              */
6 /* Inclusive Design Institute                                   */
7 /* http://atutor.ca                                                                                             */
8 /*                                                              */
9 /* This program is free software. You can redistribute it and/or*/
10 /* modify it under the terms of the GNU General Public License  */
11 /* as published by the Free Software Foundation.                                */
12 /****************************************************************/
13 // $Id$
14
15 define('AT_INCLUDE_PATH', '../../');
16 require(AT_INCLUDE_PATH.'../mods/_standard/tests/classes/testQuestions.class.php');
17 require(AT_INCLUDE_PATH.'../mods/_core/imsqti/classes/QTIParser.class.php');    
18
19 /**
20 * QTIImport
21 * Class for prehandling the POST values before importing each QTI question into ATutor
22 * Some definitions for the QTI question type: ///
23 *       1       Multiple choices
24 *       2       True/false
25 *       3       Open ended
26 *       4       Likert
27 *       5       Simple Matching
28 *       6       Ordering
29 *       7       Multiple Answers
30 *       8       Graphical Matching
31 * @access       public
32 * @author       Harris Wong
33 */
34 class QTIImport {
35         var $qti_params  = array();
36         var $qid                 = array();             //store the question_id that is generated by this import
37         var $import_path = '';
38         var $title               = '';
39         var $weights     = array();
40
41         //Constructor
42         function QTIImport($import_path){
43                 $this->import_path = $import_path;
44         }
45
46         //Creates the parameters array for TestQuestion::importQTI
47         function constructParams($qti_params){
48                 global $addslashes;
49                 //save guarding
50                 $qti_params['required']         = intval($qti_params['required']);
51                 $qti_params['question']         = trim($qti_params['question']);
52                 $qti_params['category_id']      = intval($qti_params['category_id']);
53                 $qti_params['feedback']         = trim($qti_params['feedback']);
54
55                 //assign answers
56                 if (sizeof($qti_params['answers']) > 1){
57                         $qti_params['answer'] = $qti_params['answers'];
58                 } elseif (sizeof($qti_params['answers'])==1) {
59                         $qti_params['answer'] = intval($qti_params['answers'][0]);
60                 }
61                 $this->qti_params = $qti_params;
62         }
63         
64         //Decide which question type to import based in the integer
65         function getQuestionType($question_type){
66                 $qti_obj = TestQuestions::getQuestion($question_type);
67                 if ($qti_obj != null){
68                         $qid = $qti_obj->importQTI($this->qti_params);
69                         if ($qid  > 0) {
70                                 $this->qid = $qid;
71                         }
72                 }
73         }
74
75
76         /**
77          * This function will add the attributes that are extracted from the qti xml
78          * into the database.
79          *
80          * @param       array   attributes that are extracted from the QTI XML.
81          * @return      int             the question ids.
82          */
83         function importQuestions($attributes){
84                 global $supported_media_type, $msg;
85                 $qids = array();
86
87                 foreach($attributes as $resource=>$attrs){
88                         if (preg_match('/imsqti\_(.*)/', $attrs['type'])){
89                                 //Instantiate class obj
90                                 $xml = new QTIParser($attrs['type']);
91                                 $xml_content = @file_get_contents($this->import_path . $attrs['href']);
92                                 $xml->setRelativePath($package_base_name);
93
94                                 if (!$xml->parse($xml_content)){        
95                                         $msg->addError('QTI_WRONG_PACKAGE');
96                                         break;
97                                 }
98
99                                 //set test title
100                                 $this->title = $xml->title;
101
102 //if ($attrs[href] =='56B1BEDC-A820-7AA8-A21D-F32017189445/56B1BEDC-A820-7AA8-A21D-F32017189445.xml'){
103 //      debug($xml, 'attributes');
104 //}
105                                 //import file, should we use file href? or jsut this href?
106                                 //Aug 25, use both, so then it can check for respondus media as well.
107                                 foreach($attrs['file'] as $file_id => $file_name){
108                                         $file_pathinfo = pathinfo($file_name);
109                                         if ($file_pathinfo['basename'] == $attrs['href']){
110                                                 //This file will be parsed later
111                                                 continue;
112                                         } 
113
114                                         if (in_array(strtolower($file_pathinfo['extension']), $supported_media_type)){
115                                                 //copy medias over.
116                                                 $this->copyMedia(array($file_name), $xml->items);
117                                         }
118                                 }               
119
120                                 for ($loopcounter=0; $loopcounter<$xml->item_num; $loopcounter++){
121                                         //Create POST values.
122                                         unset($test_obj);               //clear cache
123                                         $test_obj['required']           = 1;
124                                         $test_obj['preset_num'] = 0;
125                                         $test_obj['category_id']        = 0;
126                                         $test_obj['question']           = $xml->question[$loopcounter];
127                                         $test_obj['feedback']           = $xml->feedback[$loopcounter];
128                                         $test_obj['groups']             = $xml->groups[$loopcounter];
129                                         $test_obj['property']           = intval($xml->attributes[$loopcounter]['render_fib']['property']);
130                                         $test_obj['choice']             = array();
131                                         $test_obj['answers']            = array();
132
133                                         //assign choices
134                                         $i = 0;
135
136                                         //trim values
137                                         if (is_array($xml->answers[$loopcounter])){
138                                                 array_walk($xml->answers[$loopcounter], 'trim_value');
139                                         }
140                                         //TODO: The groups is 1-0+ choices.  So we should loop thru groups, not choices.
141                                         if (is_array($xml->choices[$loopcounter])){             
142                                                 foreach ($xml->choices[$loopcounter] as $choiceNum=>$choiceOpt){
143                                                         if (sizeof($test_obj['groups'] )>0) {
144                                                                 if (!empty($xml->answers[$loopcounter])){
145                                                                         foreach ($xml->answers[$loopcounter] as $ansNum=>$ansOpt){
146                                                                                 if ($choiceNum == $ansOpt){
147                                                                                         //Not exactly efficient, worst case N^2
148                                                                                         $test_obj['answers'][$ansNum] = $i;
149                                                                                 }                       
150                                                                         }
151                                                                 }
152                                                         } else {
153                                                                 //save answer(s)
154                                                                 if (is_array($xml->answers[$loopcounter]) && in_array($choiceNum, $xml->answers[$loopcounter])){
155                                                                         $test_obj['answers'][] = $i;
156                                                                 }               
157                                                         }
158                                                         $test_obj['choice'][] = $choiceOpt;
159                                                         $i++;
160                                                 }
161                                         }
162
163                 //                      unset($qti_import);
164                                         $this->constructParams($test_obj);
165 //debug($xml->getQuestionType($loopcounter), 'lp_'.$loopcounter);
166                                         //Create questions
167                                         $this->getQuestionType($xml->getQuestionType($loopcounter));
168
169                                         //save question id 
170                                         $qids[] = $this->qid;
171
172                                         //Dependency handling
173                                         if (!empty($attrs['dependency'])){
174                                                 $xml_items = array_merge($xml_items, $xml->items);
175                                         }
176                                 }
177
178                                 //assign title
179                                 if ($xml->title != ''){
180                                         $this->title = $xml->title;
181                                 }
182
183                                 //assign marks/weights
184                                 $this->weights = $xml->weights;
185
186                                 $xml->close();
187                         } elseif ($attrs['type'] == 'webcontent') {
188                                 //webcontent, copy it over.
189                                 $this->copyMedia($attrs['file'], $xml_items);
190                         }
191                 }
192 //debug($qids, 'qids');
193                 return $qids;
194         }
195
196         /**
197          * This function is to import a test and returns the test id.
198          * @param       string  custmom test title
199          *
200          * @return      int             test id
201          */
202         function importTest($title='') {
203                 global $msg, $db;
204
205                 $missing_fields                         = array();
206                 $test_obj['title']                      = ($title=='')?$this->title:$title;
207                 $test_obj['description']        = '';
208                 $test_obj['num_questions']      = 0;
209                 $test_obj['num_takes']          = 0;
210                 $test_obj['content_id']         = 0;
211                 $test_obj['passpercent']        = 0;
212                 $test_obj['passscore']          = 0;
213                 $test_obj['passfeedback']       = 0;
214                 $test_obj['failfeedback']       = 0;
215                 $test_obj['num_takes']          = 0;
216                 $test_obj['anonymous']          = 0;
217                 $test_obj['allow_guests']       = $_POST['allow_guests'] ? 1 : 0;
218                 $test_obj['instructions']       = '';
219                 $test_obj['display']            = 0;
220                 $test_obj['result_release']     = 0;
221                 $test_obj['random']                     = 0;
222
223                 // currently these options are ignored for tests:
224                 $test_obj['format']                     = intval($test_obj['format']);
225                 $test_obj['order']                      = 1;  //intval($test_obj['order']);
226                 $test_obj['difficulty']         = 0;  //intval($test_obj['difficulty']);        /* avman */
227                         
228                 //Title of the test is empty, could be from question database export or some other system's export.
229                 //Either prompt for a title, or generate a random title
230                 if ($test_obj['title'] == '') {
231                         if ($this->title != '') {
232                                 $test_obj['title'] = $this->title;
233                         } else {
234 //                              $test_obj['title'] = 'random title';
235                                 
236                                 //set marks to 0 if no title? 
237                                 $this->weights = array();
238                         }
239                 }
240
241                 /*
242                 if ($test_obj['random'] && !$test_obj['num_questions']) {
243                         $missing_fields[] = _AT('num_questions_per_test');
244                 }
245
246                 if ($test_obj['pass_score']==1 && !$test_obj['passpercent']) {
247                         $missing_fields[] = _AT('percentage_score');
248                 }
249
250                 if ($test_obj['pass_score']==2 && !$test_obj['passscore']) {
251                         $missing_fields[] = _AT('points_score');
252                 }
253
254                 if ($missing_fields) {
255                         $missing_fields = implode(', ', $missing_fields);
256                         $msg->addError(array('EMPTY_FIELDS', $missing_fields));
257                 }
258                 */
259
260                 $day_start      = intval(date('j'));
261                 $month_start= intval(date('n'));
262                 $year_start     = intval(date('Y'));
263                 $hour_start     = intval(date('G'));
264                 $min_start      = intval(date('i'));
265
266                 $day_end        = $day_start;
267                 $month_end      = $month_start;
268                 $year_end       = $year_start;  //as of Oct 21,09. Check http://www.atutor.ca/atutor/mantis/view.php?id=3961
269                 $hour_end       = $hour_start;
270                 $min_end        = $min_start;
271
272                 if (!checkdate($month_start, $day_start, $year_start)) {
273                         $msg->addError('START_DATE_INVALID');
274                 }
275
276                 if (!checkdate($month_end, $day_end, $year_end)) {
277                         $msg->addError('END_DATE_INVALID');
278                 }
279
280                 if (mktime($hour_end,   $min_end,   0, $month_end,   $day_end,   $year_end) < 
281                         mktime($hour_start, $min_start, 0, $month_start, $day_start, $year_start)) {
282                                 $msg->addError('END_DATE_INVALID');
283                 }
284
285                 if (!$msg->containsErrors()) {
286                         if (strlen($month_start) == 1){
287                                 $month_start = "0$month_start";
288                         }
289                         if (strlen($day_start) == 1){
290                                 $day_start = "0$day_start";
291                         }
292                         if (strlen($hour_start) == 1){
293                                 $hour_start = "0$hour_start";
294                         }
295                         if (strlen($min_start) == 1){
296                                 $min_start = "0$min_start";
297                         }
298
299                         if (strlen($month_end) == 1){
300                                 $month_end = "0$month_end";
301                         }
302                         if (strlen($day_end) == 1){
303                                 $day_end = "0$day_end";
304                         }
305                         if (strlen($hour_end) == 1){
306                                 $hour_end = "0$hour_end";
307                         }
308                         if (strlen($min_end) == 1){
309                                 $min_end = "0$min_end";
310                         }
311
312                         $start_date = "$year_start-$month_start-$day_start $hour_start:$min_start:00";
313                         $end_date       = "$year_end-$month_end-$day_end $hour_end:$min_end:00";
314
315                         //If title exceeded database defined length, truncate it.
316                         $test_obj['title'] = validate_length($test_obj['title'], 100);
317
318                         $sql_params = array (   $_SESSION['course_id'], 
319                                                                         $test_obj['title'], 
320                                                                         $test_obj['description'], 
321                                                                         $test_obj['format'], 
322                                                                         $start_date, 
323                                                                         $end_date, 
324                                                                         $test_obj['order'], 
325                                                                         $test_obj['num_questions'], 
326                                                                         $test_obj['instructions'], 
327                                                                         $test_obj['content_id'], 
328                                                                         $test_obj['passscore'], 
329                                                                         $test_obj['passpercent'], 
330                                                                         $test_obj['passfeedback'], 
331                                                                         $test_obj['failfeedback'], 
332                                                                         $test_obj['result_release'], 
333                                                                         $test_obj['random'], 
334                                                                         $test_obj['difficulty'], 
335                                                                         $test_obj['num_takes'], 
336                                                                         $test_obj['anonymous'], 
337                                                                         '', 
338                                                                         $test_obj['allow_guests'], 
339                                                                         $test_obj['display']);
340
341                         $sql = vsprintf(AT_SQL_TEST, $sql_params);
342                         $result = mysql_query($sql, $db);
343                         $tid = mysql_insert_id($db);
344                 //debug($qti_import->weights, 'weights');                       
345                 }
346                 return $tid;
347         }
348
349
350         /*
351          * Match the XML files to the actual files found in the content, then copy the media 
352          * over to the content folder based on the actual links.  *The XML file names might not be right.
353          * @param       array   The list of file names provided by the manifest's resources
354          * @param       array   The list of relative files that is used in the question contents.  Default empty.
355          */
356         function copyMedia($files, $xml_items = array()){
357                 global $msg;
358                 foreach($files as $file_num => $file_loc){
359                         //skip test xml files
360                         if (preg_match('/tests\_[0-9]+\.xml/', $file_loc)){
361                                 continue;
362                         }
363
364                         $new_file_loc ='';
365
366                         /**
367                                 Use the items list to handle and check which path it is from, so then it won't blindly truncate 'resource/' from the path
368                                 - For any x in xml_files, any y in new_file_loc, any k in the set of strings; such that k concat x = y, then use y, else use x
369                                 - BUG: Same filename fails.  If resource/folder1/file1.jpg, and resource/file1.jpg, both will get replaced with file1.jpg
370                         */
371                         if(!empty($xml_items)){
372                                 foreach ($xml_items as $xk=>$xv){
373                                         if (($pos = strpos($file_loc, $xv))!==false){
374                                                 //address the bug mentioned aboe.
375                                                 //check if there is just one level of directory in this extra path.
376                                                 //based on the assumption that most installation are just using 'resources/' or '{FOLDER_NAME}/'
377                                                 $shortened = substr($file_loc, 0, $pos);
378                                                 $num_of_occurrences = explode('/', $shortened);
379                                                 if (sizeof($num_of_occurrences) == 2){
380                                                         $new_file_loc = $xv;
381                                                         break;
382                                                 }
383                                         } 
384                                 }
385                         }
386
387                         if ($new_file_loc==''){
388                                 $new_file_loc = $file_loc;
389                         }
390                 
391                         //Check if the file_loc has been changed, if not, don't move it, let ims class to handle it
392                         //we only want to touch the files that the test/surveys use
393                         if ($new_file_loc!=$file_loc){
394                                 //check if new folder is there, if not, create it.
395                                 createDir(AT_CONTENT_DIR .$_SESSION['course_id'].'/'.$new_file_loc );
396                                 
397                                 //copy files over
398                 //                      if (rename(AT_CONTENT_DIR . 'import/'.$_SESSION['course_id'].'/'.$file_loc, 
399                 //                              AT_CONTENT_DIR .$_SESSION['course_id'].'/'.$package_base_name.'/'.$new_file_loc) === false) {
400                                 //overwrite files
401                                 if (file_exists(AT_CONTENT_DIR .$_SESSION['course_id'].'/'.$new_file_loc)){
402                                         unlink(AT_CONTENT_DIR .$_SESSION['course_id'].'/'.$new_file_loc);
403                                 }
404                                 if (file_exists(AT_CONTENT_DIR.'import/'.$_SESSION['course_id'].'/'.$file_loc)){
405                                         if (copy(AT_CONTENT_DIR . 'import/'.$_SESSION['course_id'].'/'.$file_loc, 
406                                                 AT_CONTENT_DIR .$_SESSION['course_id'].'/'.$new_file_loc) === false) {
407                                                 //TODO: Print out file already exist error.
408                                                 if (!$msg->containsErrors()) {
409                         //                              $msg->addError('FILE_EXISTED');
410                                                 }
411                                         }
412                                 }
413                         }
414                 }
415         }
416 }
417 ?>