2 /****************************************************************/
4 /****************************************************************/
5 /* Copyright (c) 2002-2009 */
6 /* Inclusive Design Institute */
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 /****************************************************************/
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');
21 * Class for prehandling the POST values before importing each QTI question into ATutor
22 * Some definitions for the QTI question type: ///
30 * 8 Graphical Matching
35 var $qti_params = array();
36 var $qid = array(); //store the question_id that is generated by this import
37 var $import_path = '';
39 var $weights = array();
42 function QTIImport($import_path){
43 $this->import_path = $import_path;
46 //Creates the parameters array for TestQuestion::importQTI
47 function constructParams($qti_params){
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']);
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]);
61 $this->qti_params = $qti_params;
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);
77 * This function will add the attributes that are extracted from the qti xml
80 * @param array attributes that are extracted from the QTI XML.
81 * @return int the question ids.
83 function importQuestions($attributes){
84 global $supported_media_type, $msg;
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);
94 if (!$xml->parse($xml_content)){
95 $msg->addError('QTI_WRONG_PACKAGE');
100 $this->title = $xml->title;
102 //if ($attrs[href] =='56B1BEDC-A820-7AA8-A21D-F32017189445/56B1BEDC-A820-7AA8-A21D-F32017189445.xml'){
103 // debug($xml, 'attributes');
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
114 if (in_array(strtolower($file_pathinfo['extension']), $supported_media_type)){
116 $this->copyMedia(array($file_name), $xml->items);
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();
137 if (is_array($xml->answers[$loopcounter])){
138 array_walk($xml->answers[$loopcounter], 'trim_value');
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;
154 if (is_array($xml->answers[$loopcounter]) && in_array($choiceNum, $xml->answers[$loopcounter])){
155 $test_obj['answers'][] = $i;
158 $test_obj['choice'][] = $choiceOpt;
163 // unset($qti_import);
164 $this->constructParams($test_obj);
165 //debug($xml->getQuestionType($loopcounter), 'lp_'.$loopcounter);
167 $this->getQuestionType($xml->getQuestionType($loopcounter));
170 $qids[] = $this->qid;
172 //Dependency handling
173 if (!empty($attrs['dependency'])){
174 $xml_items = array_merge($xml_items, $xml->items);
179 if ($xml->title != ''){
180 $this->title = $xml->title;
183 //assign marks/weights
184 $this->weights = $xml->weights;
187 } elseif ($attrs['type'] == 'webcontent') {
188 //webcontent, copy it over.
189 $this->copyMedia($attrs['file'], $xml_items);
192 //debug($qids, 'qids');
197 * This function is to import a test and returns the test id.
198 * @param string custmom test title
200 * @return int test id
202 function importTest($title='') {
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;
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 */
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;
234 // $test_obj['title'] = 'random title';
236 //set marks to 0 if no title?
237 $this->weights = array();
242 if ($test_obj['random'] && !$test_obj['num_questions']) {
243 $missing_fields[] = _AT('num_questions_per_test');
246 if ($test_obj['pass_score']==1 && !$test_obj['passpercent']) {
247 $missing_fields[] = _AT('percentage_score');
250 if ($test_obj['pass_score']==2 && !$test_obj['passscore']) {
251 $missing_fields[] = _AT('points_score');
254 if ($missing_fields) {
255 $missing_fields = implode(', ', $missing_fields);
256 $msg->addError(array('EMPTY_FIELDS', $missing_fields));
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'));
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;
272 if (!checkdate($month_start, $day_start, $year_start)) {
273 $msg->addError('START_DATE_INVALID');
276 if (!checkdate($month_end, $day_end, $year_end)) {
277 $msg->addError('END_DATE_INVALID');
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');
285 if (!$msg->containsErrors()) {
286 if (strlen($month_start) == 1){
287 $month_start = "0$month_start";
289 if (strlen($day_start) == 1){
290 $day_start = "0$day_start";
292 if (strlen($hour_start) == 1){
293 $hour_start = "0$hour_start";
295 if (strlen($min_start) == 1){
296 $min_start = "0$min_start";
299 if (strlen($month_end) == 1){
300 $month_end = "0$month_end";
302 if (strlen($day_end) == 1){
303 $day_end = "0$day_end";
305 if (strlen($hour_end) == 1){
306 $hour_end = "0$hour_end";
308 if (strlen($min_end) == 1){
309 $min_end = "0$min_end";
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";
315 //If title exceeded database defined length, truncate it.
316 $test_obj['title'] = validate_length($test_obj['title'], 100);
318 $sql_params = array ( $_SESSION['course_id'],
320 $test_obj['description'],
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'],
334 $test_obj['difficulty'],
335 $test_obj['num_takes'],
336 $test_obj['anonymous'],
338 $test_obj['allow_guests'],
339 $test_obj['display']);
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');
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.
356 function copyMedia($files, $xml_items = array()){
358 foreach($files as $file_num => $file_loc){
359 //skip test xml files
360 if (preg_match('/tests\_[0-9]+\.xml/', $file_loc)){
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
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){
387 if ($new_file_loc==''){
388 $new_file_loc = $file_loc;
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 );
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) {
401 if (file_exists(AT_CONTENT_DIR .$_SESSION['course_id'].'/'.$new_file_loc)){
402 unlink(AT_CONTENT_DIR .$_SESSION['course_id'].'/'.$new_file_loc);
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');