b50bad61cc55be08f406bb46f81ab21b30633ea0
[acontent.git] / docs / include / classes / QTI / QTIImport.class.php
1 <?php
2 /************************************************************************/
3 /* AContent                                                             */
4 /************************************************************************/
5 /* Copyright (c) 2010                                                   */
6 /* Inclusive Design Institute                                           */
7 /*                                                                      */
8 /* This program is free software. You can redistribute it and/or        */
9 /* modify it under the terms of the GNU General Public License          */
10 /* as published by the Free Software Foundation.                        */
11 /************************************************************************/
12
13 define('TR_INCLUDE_PATH', '../../');
14 require_once(TR_INCLUDE_PATH.'classes/testQuestions.class.php');
15 require_once(TR_INCLUDE_PATH.'classes/QTI/QTIParser.class.php');        
16 require_once(TR_INCLUDE_PATH.'classes/DAO/TestsDAO.class.php');
17
18 /**
19 * QTIImport
20 * Class for prehandling the POST values before importing each QTI question into ATutor
21 * Some definitions for the QTI question type: ///
22 *       1       Multiple choices
23 *       2       True/false
24 *       3       Open ended
25 *       4       Likert
26 *       5       Simple Matching
27 *       6       Ordering
28 *       7       Multiple Answers
29 *       8       Graphical Matching
30 * @access       public
31 * @author       Harris Wong
32 */
33 class QTIImport {
34         var $qti_params  = array();
35         var $qid                 = array();             //store the question_id that is generated by this import
36         var $import_path = '';
37         var $title               = '';
38         var $weights     = array();
39
40         //Constructor
41         function QTIImport($import_path){
42                 $this->import_path = $import_path;
43         }
44
45         //Creates the parameters array for TestQuestion::importQTI
46         function constructParams($qti_params){
47                 global $addslashes;
48                 //save guarding
49                 $qti_params['required']         = intval($qti_params['required']);
50                 $qti_params['question']         = trim($qti_params['question']);
51                 $qti_params['category_id']      = intval($qti_params['category_id']);
52                 $qti_params['feedback']         = trim($qti_params['feedback']);
53
54                 //assign answers
55                 if (sizeof($qti_params['answers']) > 1){
56                         $qti_params['answer'] = $qti_params['answers'];
57                 } elseif (sizeof($qti_params['answers'])==1) {
58                         $qti_params['answer'] = intval($qti_params['answers'][0]);
59                 }
60                 $this->qti_params = $qti_params;
61         }
62         
63         //Decide which question type to import based in the integer
64         function getQuestionType($question_type){
65                 $qti_obj = TestQuestions::getQuestion($question_type);
66                 if ($qti_obj != null){
67                         $qid = $qti_obj->importQTI($this->qti_params);
68                         if ($qid  > 0) {
69                                 $this->qid = $qid;
70                         }
71                 }
72         }
73
74
75         /**
76          * This function will add the attributes that are extracted from the qti xml
77          * into the database.
78          *
79          * @param       array   attributes that are extracted from the QTI XML.
80          * @return      int             the question ids.
81          */
82         function importQuestions($attributes){
83                 global $supported_media_type, $msg;
84                 $qids = array();
85
86                 foreach($attributes as $resource=>$attrs){
87                         if (preg_match('/imsqti\_(.*)/', $attrs['type'])){
88                                 //Instantiate class obj
89                                 $xml = new QTIParser($attrs['type']);
90                                 $xml_content = @file_get_contents($this->import_path . $attrs['href']);
91                                 $xml->setRelativePath($package_base_name);
92
93                                 if (!$xml->parse($xml_content)){        
94                                         $msg->addError('QTI_WRONG_PACKAGE');
95                                         break;
96                                 }
97
98                                 //set test title
99                                 $this->title = $xml->title;
100
101 //if ($attrs[href] =='56B1BEDC-A820-7AA8-A21D-F32017189445/56B1BEDC-A820-7AA8-A21D-F32017189445.xml'){
102 //      debug($xml, 'attributes');
103 //}
104                                 //import file, should we use file href? or jsut this href?
105                                 //Aug 25, use both, so then it can check for respondus media as well.
106                                 foreach($attrs['file'] as $file_id => $file_name){
107                                         $file_pathinfo = pathinfo($file_name);
108                                         if ($file_pathinfo['basename'] == $attrs['href']){
109                                                 //This file will be parsed later
110                                                 continue;
111                                         } 
112
113                                         if (in_array(strtolower($file_pathinfo['extension']), $supported_media_type)){
114                                                 //copy medias over.
115                                                 $this->copyMedia(array($file_name), $xml->items);
116                                         }
117                                 }               
118
119                                 for ($loopcounter=0; $loopcounter<$xml->item_num; $loopcounter++){
120                                         //Create POST values.
121                                         unset($test_obj);               //clear cache
122                                         $test_obj['required']           = 1;
123                                         $test_obj['preset_num'] = 0;
124                                         $test_obj['category_id']        = 0;
125                                         $test_obj['question']           = $xml->question[$loopcounter];
126                                         $test_obj['feedback']           = $xml->feedback[$loopcounter];
127                                         $test_obj['groups']             = $xml->groups[$loopcounter];
128                                         $test_obj['property']           = intval($xml->attributes[$loopcounter]['render_fib']['property']);
129                                         $test_obj['choice']             = array();
130                                         $test_obj['answers']            = array();
131
132                                         //assign choices
133                                         $i = 0;
134
135                                         //trim values
136                                         if (is_array($xml->answers[$loopcounter])){
137                                                 array_walk($xml->answers[$loopcounter], 'trim_value');
138                                         }
139                                         //TODO: The groups is 1-0+ choices.  So we should loop thru groups, not choices.
140                                         if (is_array($xml->choices[$loopcounter])){             
141                                                 foreach ($xml->choices[$loopcounter] as $choiceNum=>$choiceOpt){
142                                                         if (sizeof($test_obj['groups'] )>0 && is_array($xml->answers[$loopcounter])) {
143                                                                 if (!empty($xml->answers[$loopcounter])){
144                                                                         foreach ($xml->answers[$loopcounter] as $ansNum=>$ansOpt){
145                                                                                 if ($choiceNum == $ansOpt){
146                                                                                         //Not exactly efficient, worst case N^2
147                                                                                         $test_obj['answers'][$ansNum] = $i;
148                                                                                 }                       
149                                                                         }
150                                                                 }
151                                                         } else {
152                                                                 //save answer(s)
153                                                                 if (is_array($xml->answers[$loopcounter]) && in_array($choiceNum, $xml->answers[$loopcounter])){
154                                                                         $test_obj['answers'][] = $i;
155                                                                 }               
156                                                         }
157                                                         $test_obj['choice'][] = $choiceOpt;
158                                                         $i++;
159                                                 }
160                                         }
161
162                 //                      unset($qti_import);
163                                         $this->constructParams($test_obj);
164 //debug($xml->getQuestionType($loopcounter), 'lp_'.$loopcounter);
165                                         //Create questions
166                                         $this->getQuestionType($xml->getQuestionType($loopcounter));
167
168                                         //save question id 
169                                         $qids[] = $this->qid;
170
171                                         //Dependency handling
172                                         if (!empty($attrs['dependency'])){
173                                                 $xml_items = array_merge($xml_items, $xml->items);
174                                         }
175                                 }
176
177                                 //assign title
178                                 if ($xml->title != ''){
179                                         $this->title = $xml->title;
180                                 }
181
182                                 //assign marks/weights
183                                 $this->weights = $xml->weights;
184
185                                 $xml->close();
186                         } elseif ($attrs['type'] == 'webcontent') {
187                                 //webcontent, copy it over.
188                                 $this->copyMedia($attrs['file'], $xml_items);
189                         }
190                 }
191 //debug($qids, 'qids');
192                 return $qids;
193         }
194
195         /**
196          * This function is to import a test and returns the test id.
197          * @param       string  custmom test title
198          *
199          * @return      int             test id
200          */
201         function importTest($title='') {
202                 global $_course_id;
203                 
204                 $title = ($title=='')?$this->title:$title;
205                 
206                 $testsDAO = new TestsDAO();
207                 $tid = $testsDAO->Create($_course_id, 
208                                                                 $title, 
209                                                                 $test_obj['description']);
210
211 //                      $sql_params = array (   $_SESSION['course_id'], 
212 //                                                                      $test_obj['title'], 
213 //                                                                      $test_obj['description'], 
214 //                                                                      $test_obj['format'], 
215 //                                                                      $start_date, 
216 //                                                                      $end_date, 
217 //                                                                      $test_obj['order'], 
218 //                                                                      $test_obj['num_questions'], 
219 //                                                                      $test_obj['instructions'], 
220 //                                                                      $test_obj['content_id'], 
221 //                                                                      $test_obj['passscore'], 
222 //                                                                      $test_obj['passpercent'], 
223 //                                                                      $test_obj['passfeedback'], 
224 //                                                                      $test_obj['failfeedback'], 
225 //                                                                      $test_obj['result_release'], 
226 //                                                                      $test_obj['random'], 
227 //                                                                      $test_obj['difficulty'], 
228 //                                                                      $test_obj['num_takes'], 
229 //                                                                      $test_obj['anonymous'], 
230 //                                                                      '', 
231 //                                                                      $test_obj['allow_guests'], 
232 //                                                                      $test_obj['display']);
233 //
234 //                      $sql = vsprintf(AT_SQL_TEST, $sql_params);
235 //                      $result = mysql_query($sql, $db);
236 //                      $tid = mysql_insert_id($db);
237                 //debug($qti_import->weights, 'weights');                       
238                 return $tid;
239         }
240
241
242         /*
243          * Match the XML files to the actual files found in the content, then copy the media 
244          * over to the content folder based on the actual links.  *The XML file names might not be right.
245          * @param       array   The list of file names provided by the manifest's resources
246          * @param       array   The list of relative files that is used in the question contents.  Default empty.
247          */
248         function copyMedia($files, $xml_items = array()){
249                 global $msg, $_course_id;
250                 foreach($files as $file_num => $file_loc){
251                         //skip test xml files
252                         if (preg_match('/tests\_[0-9]+\.xml/', $file_loc)){
253                                 continue;
254                         }
255
256                         $new_file_loc ='';
257
258                         /**
259                                 Use the items list to handle and check which path it is from, so then it won't blindly truncate 'resource/' from the path
260                                 - 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
261                                 - BUG: Same filename fails.  If resource/folder1/file1.jpg, and resource/file1.jpg, both will get replaced with file1.jpg
262                         */
263                         if(!empty($xml_items)){
264                                 foreach ($xml_items as $xk=>$xv){
265                                         if (($pos = strpos($file_loc, $xv))!==false){
266                                                 //address the bug mentioned aboe.
267                                                 //check if there is just one level of directory in this extra path.
268                                                 //based on the assumption that most installation are just using 'resources/' or '{FOLDER_NAME}/'
269                                                 $shortened = substr($file_loc, 0, $pos);
270                                                 $num_of_occurrences = explode('/', $shortened);
271                                                 if (sizeof($num_of_occurrences) == 2){
272                                                         $new_file_loc = $xv;
273                                                         break;
274                                                 }
275                                         } 
276                                 }
277                         }
278
279                         if ($new_file_loc==''){
280                                 $new_file_loc = $file_loc;
281                         }
282                 
283                         //Check if the file_loc has been changed, if not, don't move it, let ims class to handle it
284                         //we only want to touch the files that the test/surveys use
285                         if ($new_file_loc!=$file_loc){
286                                 //check if new folder is there, if not, create it.
287                                 createDir(TR_CONTENT_DIR .$_course_id.'/'.$new_file_loc );
288                                 
289                                 //overwrite files
290                                 if (file_exists(TR_CONTENT_DIR .$_course_id.'/'.$new_file_loc)){
291                                         unlink(TR_CONTENT_DIR .$_course_id.'/'.$new_file_loc);
292                                 }
293                                 if (file_exists($this->import_path.$file_loc)){
294                                         if (copy($this->import_path.$file_loc, 
295                                                 TR_CONTENT_DIR .$_course_id.'/'.$new_file_loc) === false) {
296                                                 //TODO: Print out file already exist error.
297                                                 if (!$msg->containsErrors()) {
298                         //                              $msg->addError('FILE_EXISTED');
299                                                 }
300                                         }
301                                 }
302                         }
303                 }
304         }
305 }
306 ?>