2 /****************************************************************/
4 /****************************************************************/
5 /* Copyright (c) 2002-2008 by Greg Gay & Joel Kronenberg */
6 /* Adaptive Technology Resource Centre / University of Toronto */
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 /****************************************************************/
14 require(AT_INCLUDE_PATH.'lib/test_question_queries.inc.php');
17 * Steps to follow when adding a new question type:
19 * 1 - Create a class extending AbstractQuestion or extend an
20 * existing question class.
21 * Define $sPrefix and $sNameVar appropriately.
22 * Implement the following methods, which set template variables:
24 * assignQTIVariables()
25 * assignDisplayResultVariables()
26 * assignDisplayVariables()
27 * assignDisplayStatisticsVariables()
30 * And implement mark() which is used for marking the result.
32 * 2 - Add the new class name to $question_classes in test_question_factory()
34 * 3 - Add $sNameVar to the language database.
36 * 4 - Create the following files for creating and editing the question,
37 * where "{PREFIX}" is the value defined by $sPrefix:
39 * /tools/tests/create_question_{PREFIX}.php
40 * /tools/tests/edit_question_{PREFIX}.php
42 * 5 - Add those two newly created pages to
43 * /mods/_standard/tests/module.php
45 * 6 - Create the following template files:
47 * /themes/default/test_questions/{PREFIX}.tmpl.php
48 * /themes/default/test_questions/{PREFIX}_qti_2p1.tmpl.php
49 * /themes/default/test_questions/{PREFIX}_result.tmpl.php
50 * /themes/default/test_questions/{PREFIX}_stats.tmpl.php
52 * 7 - Add the new question type to qti import/export tools,
53 * Implement the following methods, which set template variables:
55 * include/classes/QTI/QTIParser.class.php
61 // returns array of prefix => name, sorted!
62 /*static */function getQuestionPrefixNames() {
63 $question_prefix_names = array(); // prefix => name
64 $questions = TestQuestions::getQuestionClasses();
65 foreach ($questions as $type => $question) {
66 $o = TestQuestions::getQuestion($type);
67 $question_prefix_names[$o->getPrefix()] = $o->getName();
69 asort($question_prefix_names);
70 return $question_prefix_names;
73 /*static */function getQuestionClasses() {
74 /** NOTE: The indices are CONSTANTS. Do NOT change!! **/
75 $question_classes = array(); // type ID => class name
76 $question_classes[1] = 'MultichoiceQuestion';
77 $question_classes[2] = 'TruefalseQuestion';
78 $question_classes[3] = 'LongQuestion';
79 $question_classes[4] = 'LikertQuestion';
80 $question_classes[5] = 'MatchingQuestion';
81 $question_classes[6] = 'OrderingQuestion';
82 $question_classes[7] = 'MultianswerQuestion';
83 $question_classes[8] = 'MatchingddQuestion';
85 return $question_classes;
89 * Used to create question objects based on $question_type.
90 * A singleton that creates one obj per question since
91 * questions are all stateless.
92 * Returns a reference to the question object.
94 /*static */function & getQuestion($question_type) {
95 static $objs, $question_classes;
97 if (isset($objs[$question_type])) {
98 return $objs[$question_type];
101 $question_classes = TestQuestions::getQuestionClasses();
103 if (isset($question_classes[$question_type])) {
105 $objs[$question_type] =& new $question_classes[$question_type]($savant);
110 return $objs[$question_type];
115 * Export test questions
116 * @param array an array consist of all the ids of the questions in which we desired to export.
118 function test_question_qti_export_v2p1($question_ids) {
119 require(AT_INCLUDE_PATH.'classes/zipfile.class.php'); // for zipfile
120 require(AT_INCLUDE_PATH.'lib/html_resource_parser.inc.php'); // for get_html_resources()
121 require(AT_INCLUDE_PATH.'classes/XML/XML_HTMLSax/XML_HTMLSax.php'); // for XML_HTMLSax
123 global $savant, $db, $system_courses, $languageManager;
125 $course_language = $system_courses[$_SESSION['course_id']]['primary_language'];
126 $courseLanguage =& $languageManager->getLanguage($course_language);
127 $course_language_charset = $courseLanguage->getCharacterSet();
129 $zipfile = new zipfile();
130 $zipfile->create_dir('resources/'); // for all the dependency files
131 $resources = array();
132 $dependencies = array();
134 asort($question_ids);
136 $question_ids_delim = implode(',',$question_ids);
138 $sql = "SELECT * FROM ".TABLE_PREFIX."tests_questions WHERE course_id=$_SESSION[course_id] AND question_id IN($question_ids_delim)";
139 $result = mysql_query($sql, $db);
140 while ($row = mysql_fetch_assoc($result)) {
141 $obj = TestQuestions::getQuestion($row['type']);
142 $xml = $obj->exportQTI($row, $course_language_charset, '2.1');
143 $local_dependencies = array();
145 $text_blob = implode(' ', $row);
146 $local_dependencies = get_html_resources($text_blob);
147 $dependencies = array_merge($dependencies, $local_dependencies);
149 $resources[] = array('href' => 'question_'.$row['question_id'].'.xml',
150 'dependencies' => array_keys($local_dependencies));
153 $savant->assign('xml_content', $xml);
154 $savant->assign('title', $row['question']);
155 $xml = $savant->fetch('test_questions/wrapper.tmpl.php');
157 $zipfile->add_file($xml, 'question_'.$row['question_id'].'.xml');
160 // add any dependency files:
161 foreach ($dependencies as $resource => $resource_server_path) {
162 $zipfile->add_file(@file_get_contents($resource_server_path), 'resources/' . $resource, filemtime($resource_server_path));
165 // construct the manifest xml
166 $savant->assign('resources', $resources);
167 $savant->assign('dependencies', array_keys($dependencies));
168 $savant->assign('encoding', $course_language_charset);
169 $manifest_xml = $savant->fetch('test_questions/manifest_qti_2p1.tmpl.php');
171 $zipfile->add_file($manifest_xml, 'imsmanifest.xml');
175 $filename = str_replace(array(' ', ':'), '_', $_SESSION['course_title'].'-'._AT('question_database').'-'.date('Ymd'));
176 $zipfile->send_file($filename);
182 * Export test questions
183 * @param array an array consist of all the ids of the questions in which we desired to export.
185 function test_question_qti_export($question_ids) {
186 require(AT_INCLUDE_PATH.'classes/zipfile.class.php'); // for zipfile
187 require(AT_INCLUDE_PATH.'lib/html_resource_parser.inc.php'); // for get_html_resources()
188 require(AT_INCLUDE_PATH.'classes/XML/XML_HTMLSax/XML_HTMLSax.php'); // for XML_HTMLSax
190 global $savant, $db, $system_courses, $languageManager;
192 $course_language = $system_courses[$_SESSION['course_id']]['primary_language'];
193 $courseLanguage =& $languageManager->getLanguage($course_language);
194 $course_language_charset = $courseLanguage->getCharacterSet();
196 $zipfile = new zipfile();
197 $zipfile->create_dir('resources/'); // for all the dependency files
198 $resources = array();
199 $dependencies = array();
201 asort($question_ids);
203 $question_ids_delim = implode(',',$question_ids);
205 $sql = "SELECT * FROM ".TABLE_PREFIX."tests_questions WHERE course_id=$_SESSION[course_id] AND question_id IN($question_ids_delim)";
206 $result = mysql_query($sql, $db);
207 while ($row = mysql_fetch_assoc($result)) {
208 $obj = TestQuestions::getQuestion($row['type']);
210 $local_xml = $obj->exportQTI($row, $course_language_charset, '1.2.1');
211 $local_dependencies = array();
213 $text_blob = implode(' ', $row);
214 $local_dependencies = get_html_resources($text_blob);
215 $dependencies = array_merge($dependencies, $local_dependencies);
217 // $resources[] = array('href' => 'question_'.$row['question_id'].'.xml',
218 // 'dependencies' => array_keys($local_dependencies));
220 $xml = $xml . "\n\n" . $local_xml;
225 $savant->assign('xml_content', $xml);
226 $savant->assign('title', $row['question']);
227 $xml = $savant->fetch('test_questions/wrapper.tmpl.php');
229 $xml_filename = 'atutor_questions.xml';
230 $zipfile->add_file($xml, $xml_filename);
232 // add any dependency files:
233 foreach ($dependencies as $resource => $resource_server_path) {
234 $zipfile->add_file(@file_get_contents($resource_server_path), 'resources/' . $resource, filemtime($resource_server_path));
237 // construct the manifest xml
238 // $savant->assign('resources', $resources);
239 $savant->assign('dependencies', array_keys($dependencies));
240 $savant->assign('encoding', $course_language_charset);
241 $savant->assign('xml_filename', $xml_filename);
242 // $manifest_xml = $savant->fetch('test_questions/manifest_qti_2p1.tmpl.php');
243 $manifest_xml = $savant->fetch('test_questions/manifest_qti_1p2.tmpl.php');
245 $zipfile->add_file($manifest_xml, 'imsmanifest.xml');
249 $filename = str_replace(array(' ', ':'), '_', $_SESSION['course_title'].'-'._AT('question_database').'-'.date('Ymd'));
250 $zipfile->send_file($filename);
258 * @param string the test title
259 * @param ref [OPTIONAL] zip object reference
260 * @param array [OPTIONAL] list of already added files.
262 function test_qti_export($tid, $test_title='', $zipfile = null){
263 require_once(AT_INCLUDE_PATH.'classes/zipfile.class.php'); // for zipfile
264 require_once(AT_INCLUDE_PATH.'classes/XML/XML_HTMLSax/XML_HTMLSax.php'); // for XML_HTMLSax
265 require_once(AT_INCLUDE_PATH.'lib/html_resource_parser.inc.php'); // for get_html_resources()
266 global $savant, $db, $system_courses, $languageManager, $test_zipped_files, $test_files, $use_cc;
269 $course_language = $system_courses[$_SESSION['course_id']]['primary_language'];
270 $courseLanguage =& $languageManager->getLanguage($course_language);
271 $course_language_charset = $courseLanguage->getCharacterSet();
279 if ($test_zipped_files == null){
280 $test_zipped_files = array();
284 $zipfile = new zipfile();
285 $zipfile->create_dir('resources/'); // for all the dependency files
287 $resources = array();
288 $dependencies = array();
290 // don't want to sort it, i want the same order out.
291 // asort($question_ids);
293 //TODO: Merge the following 2 sqls together.
294 //Randomized or not, export all the questions that are associated with it.
295 $sql = "SELECT TQ.question_id, TQA.weight FROM ".TABLE_PREFIX."tests_questions TQ INNER JOIN ".TABLE_PREFIX."tests_questions_assoc TQA USING (question_id) WHERE TQ.course_id=$_SESSION[course_id] AND TQA.test_id=$tid ORDER BY TQA.ordering, TQA.question_id";
296 $result = mysql_query($sql, $db);
297 $question_ids = array();
299 while (($question_row = mysql_fetch_assoc($result)) != false){
300 $question_ids[] = $question_row['question_id'];
302 $question_ids_delim = implode(',',$question_ids);
304 //No questions in the test
305 if (sizeof($question_ids_delim)==0){
309 //$sql = "SELECT * FROM ".TABLE_PREFIX."tests_questions WHERE course_id=$_SESSION[course_id] AND question_id IN($question_ids_delim)";
310 $sql = "SELECT TQ.*, TQA.weight, TQA.test_id FROM ".TABLE_PREFIX."tests_questions TQ INNER JOIN ".TABLE_PREFIX."tests_questions_assoc TQA USING (question_id) WHERE TQA.test_id=$tid AND TQ.question_id IN($question_ids_delim) ORDER BY TQA.ordering, TQA.question_id";
312 $result = mysql_query($sql, $db);
313 while ($row = mysql_fetch_assoc($result)) {
314 $obj = TestQuestions::getQuestion($row['type']);
316 $local_xml = $obj->exportQTI($row, $course_language_charset, '1.2.1');
317 $local_dependencies = array();
319 $text_blob = implode(' ', $row);
320 $local_dependencies = get_html_resources($text_blob);
321 $dependencies = array_merge($dependencies, $local_dependencies);
323 $xml = $xml . "\n\n" . $local_xml;
326 //files that are found inside the test; used by print_organization(), to add all test files into QTI/ folder.
327 $test_files = $dependencies;
329 $resources[] = array('href' => 'tests_'.$tid.'.xml',
330 'dependencies' => array_keys($dependencies));
335 $sql = "SELECT title FROM ".TABLE_PREFIX."tests WHERE test_id = $tid";
336 $result = mysql_query($sql, $db);
337 $row = mysql_fetch_array($result);
339 //TODO: wrap around xml now
340 $savant->assign('xml_content', $xml);
341 $savant->assign('title', htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8'));
342 $xml = $savant->fetch('test_questions/wrapper.tmpl.php');
344 $xml_filename = 'tests_'.$tid.'.xml';
346 $zipfile->add_file($xml, $xml_filename);
348 $zipfile->add_file($xml, 'QTI/'.$xml_filename);
351 // add any dependency files:
353 foreach ($dependencies as $resource => $resource_server_path) {
354 //add this file in if it's not already in the zip package
355 if (!in_array($resource_server_path, $test_zipped_files)){
356 $zipfile->add_file(@file_get_contents($resource_server_path), 'resources/'.$resource, filemtime($resource_server_path));
357 $test_zipped_files[] = $resource_server_path;
363 // construct the manifest xml
364 $savant->assign('resources', $resources);
365 $savant->assign('dependencies', array_keys($dependencies));
366 $savant->assign('encoding', $course_language_charset);
367 $savant->assign('title', $test_title);
368 $savant->assign('xml_filename', $xml_filename);
370 $manifest_xml = $savant->fetch('test_questions/manifest_qti_1p2.tmpl.php');
371 $zipfile->add_file($manifest_xml, 'imsmanifest.xml');
375 $filename = str_replace(array(' ', ':'), '_', $_SESSION['course_title'].'-'.$test_title.'-'.date('Ymd'));
376 $zipfile->send_file($filename);
380 $return_array[$xml_filename] = array_keys($dependencies);
381 return $return_array;
387 * Recursively create folders
388 * For the purpose of this webapp only. All the paths are seperated by a /
389 * And thus this function will loop through each directory and create them on the way
390 * if it doesn't exist.
393 function recursive_mkdir($path, $mode = 0700) {
394 $dirs = explode(DIRECTORY_SEPARATOR , $path);
395 $count = count($dirs);
397 for ($i = 0; $i < $count; ++$i) {
398 $path .= $dirs[$i].DIRECTORY_SEPARATOR;
399 //If the directory has not been created, create it and return error on failure
400 if (!is_dir($path) && !mkdir($path, $mode)) {
409 * keeps count of the question number (when displaying the question)
410 * need this function because PHP 4 doesn't support static members
412 function TestQuestionCounter($increment = FALSE) {
415 if (!isset($count)) {
429 * Note that all PHP 5 OO declarations and signatures are commented out to be
430 * backwards compatible with PHP 4.
433 /*abstract */ class AbstractTestQuestion {
435 * Savant2 $savant - refrence to the savant obj
437 /*protected */ var $savant;
440 * Constructor method. Initialises variables.
442 function AbstractTestQuestion(&$savant) { $this->savant =& $savant; }
447 /*final public */function seed($salt) {
449 * by controlling the seed before calling array_rand() we insure that
450 * we can un-randomize the order for marking.
451 * used with ordering type questions only.
453 srand($salt + $_SESSION['member_id']);
459 /*final public */function unseed() {
460 // To fix http://www.atutor.ca/atutor/mantis/view.php?id=3167
461 // Disturb the seed for ordering questions after mark to avoid the deterioration
462 // of the random distribution due to a repeated initialization of the same random seed
463 list($usec, $sec) = explode(" ", microtime());
464 srand((int)($usec*10));
469 * Prints the name of this question
471 /*final public */function printName() { echo $this->getName(); }
475 * Prints the name of this question
477 /*final public */function getName() { return _AT($this->sNameVar); }
481 * Returns the prefix string (used for file names)
483 /*final public */function getPrefix() { return $this->sPrefix; }
486 * Display the current question (for taking or previewing a test/question)
488 /*final public */function display($row, $response = '') {
489 // print the generic question header
490 $this->displayHeader($row['weight']);
492 // print the question specific template
493 $this->assignDisplayVariables($row, $response);
494 $this->savant->display('test_questions/' . $this->sPrefix . '.tmpl.php');
496 // print the generic question footer
497 $this->displayFooter();
501 * Display the result for the current question
503 /*final public */function displayResult($row, $answer_row, $editable = FALSE) {
504 // print the generic question header
505 $this->displayHeader($row['weight'], $answer_row['score'], $editable ? $row['question_id'] : FALSE);
507 // print the question specific template
508 $this->assignDisplayResultVariables($row, $answer_row);
509 $this->savant->display('test_questions/' . $this->sPrefix . '_result.tmpl.php');
511 // print the generic question footer
512 $this->displayFooter();
517 * print the question template header
519 /*final public */function displayResultStatistics($row, $answers) {
520 TestQuestionCounter(TRUE);
521 $this->assignDisplayStatisticsVariables($row, $answers);
522 $this->savant->display('test_questions/' . $this->sPrefix . '_stats.tmpl.php');
525 /*final public */function exportQTI($row, $encoding, $version) {
526 $this->savant->assign('encoding', $encoding);
527 //Convert all row values to html entities
528 foreach ($row as $k=>$v){
529 $row[$k] = htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); //not using htmlentities cause it changes some languages falsely.
531 $this->assignQTIVariables($row);
532 if ($version=='2.1') {
533 $xml = $this->savant->fetch('test_questions/'. $this->sPrefix . '_qti_2p1.tmpl.php');
535 $xml = $this->savant->fetch('test_questions/'. $this->sPrefix . '_qti_1p2.tmpl.php');
541 * print the question template header
543 /*final private */function displayHeader($weight, $score = FALSE, $question_id = FALSE) {
544 TestQuestionCounter(TRUE);
546 if ($score) $score = intval($score);
547 $this->savant->assign('question_id', $question_id);
548 $this->savant->assign('score', $score);
549 $this->savant->assign('weight', $weight);
550 $this->savant->assign('type', _AT($this->sNameVar));
551 $this->savant->assign('number', TestQuestionCounter());
552 $this->savant->display('test_questions/header.tmpl.php');
556 * print the question template footer
558 /*final private */function displayFooter() {
559 $this->savant->display('test_questions/footer.tmpl.php');
563 * return only the non-empty choices from $row.
564 * assumes choices are sequential.
566 /*protected */function getChoices($row) {
568 for ($i=0; $i < 10; $i++) {
569 if ($row['choice_'.$i] != '') {
571 $choices[] = $row['choice_'.$i];
584 class OrderingQuestion extends AbstractTestQuestion {
585 /*protected */ var $sNameVar = 'test_ordering';
586 /*protected */ var $sPrefix = 'ordering';
588 /*protected */function assignDisplayResultVariables($row, $answer_row) {
589 $answers = explode('|', $answer_row['answer']);
591 $num_choices = count($this->getChoices($row));
593 $this->savant->assign('base_href', AT_BASE_HREF);
594 $this->savant->assign('num_choices', $num_choices);
595 $this->savant->assign('answers', $answers);
596 $this->savant->assign('row', $row);
599 /*protected */function assignQTIVariables($row) {
600 $choices = $this->getChoices($row);
601 $num_choices = count($choices);
603 $this->savant->assign('num_choices', $num_choices);
604 $this->savant->assign('row', $row);
607 /*protected */function assignDisplayVariables($row, $response) {
608 // determine the number of choices this question has
609 // and save those choices to be re-assigned back to $row
610 // in the randomized order.
611 $choices = $this->getChoices($row);
612 $num_choices = count($choices);
614 // response from the test_answers table is in the correct order
615 // so, they have to be re-randomized in the same order as the
616 // choices are. this is only possible because of the seed() method.
617 $response = explode('|', $response);
618 $new_response = array();
620 // randomize the order of choices and re-assign to $row
621 $this->seed($row['question_id']);
622 $rand = array_rand($choices, $num_choices);
623 for ($i=0; $i < 10; $i++) {
624 $row['choice_'.$i] = $choices[$rand[$i]];
625 $new_response[$i] = $response[$rand[$i]];
628 $this->savant->assign('num_choices', $num_choices);
629 $this->savant->assign('row', $row);
631 $this->savant->assign('response', $new_response);
634 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
636 foreach ($answers as $answer) {
637 $num_results += $answer['count'];
640 $choices = $this->getChoices($row);
641 $num_choices = count($choices);
643 $final_answers = array(); // assoc array of # of times that key was used correctly 0, 1, ... $num -1
644 foreach ($answers as $key => $value) {
645 $values = explode('|', $key);
646 // we assume $values is never empty and contains $num number of answers
647 for ($i=0; $i<=$num_choices; $i++) {
648 if ($values[$i] == $i) {
649 $final_answers[$i] += $answers[$key]['count'];
654 $this->savant->assign('num_results', $num_results);
655 $this->savant->assign('num_choices', $num_choices);
656 $this->savant->assign('answers', $final_answers);
657 $this->savant->assign('row', $row);
660 /*public */function mark($row) {
661 $this->seed($row['question_id']);
662 $num_choices = count($_POST['answers'][$row['question_id']]);
663 $answers = range(0, $num_choices-1);
664 $answers = array_rand($answers, $num_choices);
666 // Disturb the seed for ordering questions after mark to avoid the deterioration
667 // of the random distribution due to a repeated initialization of the same random seed
670 $num_answer_correct = 0;
672 $ordered_answers = array();
674 for ($i = 0; $i < $num_choices ; $i++) {
675 $_POST['answers'][$row['question_id']][$i] = intval($_POST['answers'][$row['question_id']][$i]);
677 if ($_POST['answers'][$row['question_id']][$i] == -1) {
678 // nothing to do. it was left blank
679 } else if ($_POST['answers'][$row['question_id']][$i] == $answers[$i]) {
680 $num_answer_correct++;
682 $ordered_answers[$answers[$i]] = $_POST['answers'][$row['question_id']][$i];
684 ksort($ordered_answers);
688 // to avoid roundoff errors:
689 if ($num_answer_correct == $num_choices) {
690 $score = $row['weight'];
691 } else if ($num_answer_correct > 0) {
692 $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
693 if ( (float) (int) $score == $score) {
694 $score = (int) $score; // a whole number with decimals, eg. "2.00"
696 $score = trim($score, '0'); // remove trailing zeros, if any, eg. "2.50"
700 $_POST['answers'][$row['question_id']] = implode('|', $ordered_answers);
705 //QTI Import Ordering Question
706 function importQTI($_POST){
709 if ($_POST['question'] == ''){
710 $missing_fields[] = _AT('question');
713 if (trim($_POST['choice'][0]) == '') {
714 $missing_fields[] = _AT('item').' 1';
716 if (trim($_POST['choice'][1]) == '') {
717 $missing_fields[] = _AT('item').' 2';
720 if ($missing_fields) {
721 $missing_fields = implode(', ', $missing_fields);
722 $msg->addError(array('EMPTY_FIELDS', $missing_fields));
725 if (!$msg->containsErrors()) {
726 $choice_new = array(); // stores the non-blank choices
727 $answer_new = array(); // stores the non-blank answers
728 $order = 0; // order count
729 for ($i=0; $i<10; $i++) {
731 * Db defined it to be 255 length, chop strings off it it's less than that
734 $_POST['choice'][$i] = validate_length($_POST['choice'][$i], 255);
735 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
737 if ($_POST['choice'][$i] != '') {
738 /* filter out empty choices/ remove gaps */
739 $choice_new[] = $_POST['choice'][$i];
740 $answer_new[] = $order++;
744 $_POST['choice'] = array_pad($choice_new, 10, '');
745 $answer_new = array_pad($answer_new, 10, 0);
746 // $_POST['feedback'] = $addslashes($_POST['feedback']);
747 // $_POST['question'] = $addslashes($_POST['question']);
749 $sql_params = array( $_POST['category_id'],
750 $_SESSION['course_id'],
774 $sql = vsprintf(AT_SQL_QUESTION_ORDERING, $sql_params);
776 $result = mysql_query($sql, $db);
778 return mysql_insert_id();
788 class TruefalseQuestion extends AbstracttestQuestion {
789 /*protected */ var $sPrefix = 'truefalse';
790 /*protected */ var $sNameVar = 'test_tf';
792 /*protected */function assignQTIVariables($row) {
793 $this->savant->assign('row', $row);
796 /*protected */function assignDisplayResultVariables($row, $answer_row) {
798 $this->savant->assign('base_href', AT_BASE_HREF);
799 $this->savant->assign('answers', $answer_row['answer']);
800 $this->savant->assign('row', $row);
803 /*protected */function assignDisplayVariables($row, $response) {
804 $this->savant->assign('row', $row);
805 $this->savant->assign('response', $response);
808 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
810 foreach ($answers as $answer) {
811 $num_results += $answer['count'];
814 $this->savant->assign('num_results', $num_results);
815 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
816 $this->savant->assign('num_true', (int) $answers['1']['count']);
817 $this->savant->assign('num_false', (int) $answers['2']['count']);
818 $this->savant->assign('row', $row);
821 /*public */function mark($row) {
822 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
824 if ($row['answer_0'] == $_POST['answers'][$row['question_id']]) {
825 return (int) $row['weight'];
830 //QTI Import True/False Question
831 function importQTI($_POST){
834 if ($_POST['question'] == ''){
835 $msg->addError(array('EMPTY_FIELDS', _AT('statement')));
838 //assign true answer to 1, false answer to 2, idk to 3, for ATutor
839 if ($_POST['answer'] == 'ChoiceT'){
840 $_POST['answer'] = 1;
842 $_POST['answer'] = 2;
845 if (!$msg->containsErrors()) {
846 // $_POST['feedback'] = $addslashes($_POST['feedback']);
847 // $_POST['question'] = $addslashes($_POST['question']);
850 $sql_params = array( $_POST['category_id'],
851 $_SESSION['course_id'],
856 $sql = vsprintf(AT_SQL_QUESTION_TRUEFALSE, $sql_params);
857 $result = mysql_query($sql, $db);
859 return mysql_insert_id();
869 class LikertQuestion extends AbstracttestQuestion {
870 /*protected */ var $sPrefix = 'likert';
871 /*protected */ var $sNameVar = 'test_lk';
873 /*protected */function assignQTIVariables($row) {
874 $choices = $this->getChoices($row);
875 $num_choices = count($choices);
877 $this->savant->assign('num_choices', $num_choices);
878 $this->savant->assign('row', $row);
881 /*protected */function assignDisplayResultVariables($row, $answer_row) {
882 $this->savant->assign('answer', $answer_row['answer']);
883 $this->savant->assign('row', $row);
886 /*protected */function assignDisplayVariables($row, $response) {
887 $choices = $this->getChoices($row);
888 $num_choices = count($choices);
890 $this->savant->assign('num_choices', $num_choices);
891 $this->savant->assign('row', $row);
893 if (empty($response)) {
896 $this->savant->assign('response', $response);
899 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
901 foreach ($answers as $answer) {
902 $num_results += $answer['count'];
905 $choices = $this->getChoices($row);
906 $num_choices = count($choices);
909 for ($i=0; $i<$num_choices; $i++) {
910 $sum += ($i+1) * $answers[$i]['count'];
912 $average = round($sum/$num_results, 1);
914 $this->savant->assign('num_results', $num_results);
915 $this->savant->assign('average', $average);
916 $this->savant->assign('num_choices', $num_choices);
917 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
918 $this->savant->assign('answers', $answers);
919 $this->savant->assign('row', $row);
922 /*public */function mark($row) {
923 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
927 //QTI Import Likert Question
928 function importQTI($_POST){
930 // $_POST = $this->_POST;
932 $empty_fields = array();
933 if ($_POST['question'] == ''){
934 $empty_fields[] = _AT('question');
936 if ($_POST['choice'][0] == '') {
937 $empty_fields[] = _AT('choice').' 1';
940 if ($_POST['choice'][1] == '') {
941 $empty_fields[] = _AT('choice').' 2';
944 if (!empty($empty_fields)) {
945 // $msg->addError(array('EMPTY_FIELDS', implode(', ', $empty_fields)));
948 if (!$msg->containsErrors()) {
949 $_POST['feedback'] = '';
950 // $_POST['question'] = $addslashes($_POST['question']);
952 for ($i=0; $i<10; $i++) {
953 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
954 $_POST['answer'][$i] = intval($_POST['answer'][$i]);
956 if ($_POST['choice'][$i] == '') {
957 /* an empty option can't be correct */
958 $_POST['answer'][$i] = 0;
962 $sql_params = array( $_POST['category_id'],
963 $_SESSION['course_id'],
985 $_POST['answer'][9]);
987 $sql = vsprintf(AT_SQL_QUESTION_LIKERT, $sql_params);
988 $result = mysql_query($sql, $db);
990 return mysql_insert_id();
1000 class LongQuestion extends AbstracttestQuestion {
1001 /*protected */ var $sPrefix = 'long';
1002 /*protected */ var $sNameVar = 'test_open';
1004 /*protected */function assignQTIVariables($row) {
1005 $this->savant->assign('row', $row);
1008 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1009 $this->savant->assign('answer', $answer_row['answer']);
1010 $this->savant->assign('row', $row);
1013 /*protected */function assignDisplayVariables($row, $response) {
1014 $this->savant->assign('row', $row);
1015 $this->savant->assign('response', $response);
1018 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1020 foreach ($answers as $answer) {
1021 $num_results += $answer['count'];
1024 $this->savant->assign('num_results', $num_results);
1025 $this->savant->assign('num_blanks', (int) $answers['']['count']);
1026 $this->savant->assign('answers', $answers);
1027 $this->savant->assign('row', $row);
1030 /*public */function mark($row) {
1032 $_POST['answers'][$row['question_id']] = $addslashes($_POST['answers'][$row['question_id']]);
1036 //QTI Import Open end/long Question
1037 function importQTI($_POST){
1039 // $_POST = $this->_POST;
1041 if ($_POST['question'] == ''){
1042 // $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1045 if (!$msg->containsErrors()) {
1046 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1047 // $_POST['question'] = $addslashes($_POST['question']);
1049 if ($_POST['property']==''){
1050 $_POST['property'] = 4; //essay
1053 $sql_params = array( $_POST['category_id'],
1054 $_SESSION['course_id'],
1057 $_POST['property']);
1059 $sql = vsprintf(AT_SQL_QUESTION_LONG, $sql_params);
1061 $result = mysql_query($sql, $db);
1063 return mysql_insert_id();
1073 class MatchingQuestion extends AbstracttestQuestion {
1074 /*protected */ var $sPrefix = 'matching';
1075 /*protected */ var $sNameVar = 'test_matching';
1077 /*protected */function assignQTIVariables($row) {
1078 $choices = $this->getChoices($row);
1079 $num_choices = count($choices);
1082 for ($i=0; $i < 10; $i++) {
1083 if ($row['option_'. $i] != '') {
1088 $this->savant->assign('num_choices', $num_choices);
1089 $this->savant->assign('num_options', $num_options);
1090 $this->savant->assign('row', $row);
1093 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1095 for ($i=0; $i < 10; $i++) {
1096 if ($row['option_'. $i] != '') {
1101 $answer_row['answer'] = explode('|', $answer_row['answer']);
1105 $this->savant->assign('base_href', AT_BASE_HREF);
1106 $this->savant->assign('answers', $answer_row['answer']);
1107 $this->savant->assign('letters', $_letters);
1108 $this->savant->assign('num_options', $num_options);
1109 $this->savant->assign('row', $row);
1112 /*protected */function assignDisplayVariables($row, $response) {
1113 $choices = $this->getChoices($row);
1114 $num_choices = count($choices);
1116 if (empty($response)) {
1117 $response = array_fill(0, $num_choices, -1);
1119 $response = explode('|', $response);
1123 for ($i=0; $i < 10; $i++) {
1124 if ($row['option_'. $i] != '') {
1131 $this->savant->assign('num_choices', $num_choices);
1132 $this->savant->assign('base_href', AT_BASE_HREF);
1133 $this->savant->assign('letters', $_letters);
1134 $this->savant->assign('num_options', $num_options);
1135 $this->savant->assign('row', $row);
1137 $this->savant->assign('response', $response);
1140 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1141 $choices = $this->getChoices($row);
1142 $num_choices = count($choices);
1145 foreach ($answers as $answer) {
1146 $num_results += $answer['count'];
1149 foreach ($answers as $key => $value) {
1150 $values = explode('|', $key);
1151 if (count($values) > 1) {
1152 for ($i=0; $i<count($values); $i++) {
1153 $answers[$values[$i]]['count']++;
1158 $this->savant->assign('num_choices', $num_choices);
1159 $this->savant->assign('num_results', $num_results);
1160 $this->savant->assign('answers', $answers);
1161 $this->savant->assign('row', $row);
1164 /*public */function mark($row) {
1165 $num_choices = count($_POST['answers'][$row['question_id']]);
1166 $num_answer_correct = 0;
1167 foreach ($_POST['answers'][$row['question_id']] as $item_id => $response) {
1168 if ($row['answer_' . $item_id] == $response) {
1169 $num_answer_correct++;
1171 $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
1175 // to avoid roundoff errors:
1176 if ($num_answer_correct == $num_choices) {
1177 $score = $row['weight'];
1178 } else if ($num_answer_correct > 0) {
1179 $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
1180 if ( (float) (int) $score == $score) {
1181 $score = (int) $score; // a whole number with decimals, eg. "2.00"
1183 $score = trim($score, '0'); // remove trailing zeros, if any
1187 $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
1192 //QTI Import Matching Question
1193 function importQTI($_POST){
1195 // $_POST = $this->_POST;
1197 if (!is_array($_POST['answer'])){
1198 $temp = $_POST['answer'];
1199 $_POST['answer'] = array();
1200 $_POST['answer'][0] = $temp;
1202 ksort($_POST['answer']); //array_pad returns an array disregard of the array keys
1203 //default for matching is '-'
1204 $_POST['answer']= array_pad($_POST['answer'], 10, -1);
1206 for ($i = 0 ; $i < 10; $i++) {
1207 $_POST['groups'][$i] = trim($_POST['groups'][$i]);
1208 $_POST['answer'][$i] = (int) $_POST['answer'][$i];
1209 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
1212 if (!$_POST['groups'][0]
1213 || !$_POST['groups'][1]
1214 || !$_POST['choice'][0]
1215 || !$_POST['choice'][1]) {
1216 // $msg->addError('QUESTION_EMPTY');
1219 if (!$msg->containsErrors()) {
1220 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1221 // $_POST['instructions'] = $addslashes($_POST['instructions']);
1223 $sql_params = array( $_POST['category_id'],
1224 $_SESSION['course_id'],
1227 $_POST['groups'][0],
1228 $_POST['groups'][1],
1229 $_POST['groups'][2],
1230 $_POST['groups'][3],
1231 $_POST['groups'][4],
1232 $_POST['groups'][5],
1233 $_POST['groups'][6],
1234 $_POST['groups'][7],
1235 $_POST['groups'][8],
1236 $_POST['groups'][9],
1237 $_POST['answer'][0],
1238 $_POST['answer'][1],
1239 $_POST['answer'][2],
1240 $_POST['answer'][3],
1241 $_POST['answer'][4],
1242 $_POST['answer'][5],
1243 $_POST['answer'][6],
1244 $_POST['answer'][7],
1245 $_POST['answer'][8],
1246 $_POST['answer'][9],
1247 $_POST['choice'][0],
1248 $_POST['choice'][1],
1249 $_POST['choice'][2],
1250 $_POST['choice'][3],
1251 $_POST['choice'][4],
1252 $_POST['choice'][5],
1253 $_POST['choice'][6],
1254 $_POST['choice'][7],
1255 $_POST['choice'][8],
1256 $_POST['choice'][9]);
1258 $sql = vsprintf(AT_SQL_QUESTION_MATCHINGDD, $sql_params);
1260 $result = mysql_query($sql, $db);
1262 return mysql_insert_id();
1269 * matchingddQuestion
1272 class MatchingddQuestion extends MatchingQuestion {
1273 /*protected */ var $sPrefix = 'matchingdd';
1274 /*protected */ var $sNameVar = 'test_matchingdd';
1278 * multichoiceQuestion
1281 class MultichoiceQuestion extends AbstracttestQuestion {
1282 /*protected */ var $sPrefix = 'multichoice';
1283 /*protected */var $sNameVar = 'test_mc';
1285 /*protected */function assignQTIVariables($row) {
1286 $choices = $this->getChoices($row);
1287 $num_choices = count($choices);
1289 $this->savant->assign('num_choices', $num_choices);
1290 $this->savant->assign('row', $row);
1293 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1294 if (strpos($answer_row['answer'], '|') !== false) {
1295 $answer_row['answer'] = explode('|', $answer_row['answer']);
1297 $answer_row['answer'] = array($answer_row['answer']);
1300 $this->savant->assign('base_href', AT_BASE_HREF);
1301 $this->savant->assign('answers', $answer_row['answer']);
1302 $this->savant->assign('row', $row);
1305 /*protected */function assignDisplayVariables($row, $response) {
1306 $choices = $this->getChoices($row);
1307 $num_choices = count($choices);
1309 if ($response == '') {
1312 $response = explode('|', $response);
1313 $this->savant->assign('response', $response);
1315 $this->savant->assign('num_choices', $num_choices);
1316 $this->savant->assign('row', $row);
1319 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1320 $choices = $this->getChoices($row);
1321 $num_choices = count($choices);
1324 foreach ($answers as $answer) {
1325 $num_results += $answer['count'];
1328 foreach ($answers as $key => $value) {
1329 $values = explode('|', $key);
1330 if (count($values) > 1) {
1331 for ($i=0; $i<count($values); $i++) {
1332 $answers[$values[$i]]['count']++;
1337 $this->savant->assign('num_choices', $num_choices);
1338 $this->savant->assign('num_results', $num_results);
1339 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
1340 $this->savant->assign('answers', $answers);
1341 $this->savant->assign('row', $row);
1344 /*public */function mark($row) {
1346 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
1347 if ($row['answer_' . $_POST['answers'][$row['question_id']]]) {
1348 $score = $row['weight'];
1349 } else if ($_POST['answers'][$row['question_id']] == -1) {
1351 for($i=0; $i<10; $i++) {
1352 $has_answer += $row['answer_'.$i];
1354 if (!$has_answer && $row['weight']) {
1355 // If MC has no answer and user answered "leave blank"
1356 $score = $row['weight'];
1362 //QTI Import Multiple Choice Question
1363 function importQTI($_POST){
1365 // $_POST = $this->_POST;
1366 if ($_POST['question'] == ''){
1367 $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1370 if (!$msg->containsErrors()) {
1371 // $_POST['question'] = $addslashes($_POST['question']);
1373 for ($i=0; $i<10; $i++) {
1374 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
1377 $answers = array_fill(0, 10, 0);
1378 if (is_array($_POST['answer'])){
1379 $answers[0] = 1; //default the first to be the right answer. TODO, use summation of points.
1381 $answers[$_POST['answer']] = 1;
1384 $sql_params = array( $_POST['category_id'],
1385 $_SESSION['course_id'],
1388 $_POST['choice'][0],
1389 $_POST['choice'][1],
1390 $_POST['choice'][2],
1391 $_POST['choice'][3],
1392 $_POST['choice'][4],
1393 $_POST['choice'][5],
1394 $_POST['choice'][6],
1395 $_POST['choice'][7],
1396 $_POST['choice'][8],
1397 $_POST['choice'][9],
1409 $sql = vsprintf(AT_SQL_QUESTION_MULTI, $sql_params);
1410 $result = mysql_query($sql, $db);
1412 return mysql_insert_id();
1419 * multianswerQuestion
1422 class MultianswerQuestion extends MultichoiceQuestion {
1423 /*protected */ var $sPrefix = 'multianswer';
1424 /*protected */ var $sNameVar = 'test_ma';
1426 /*public */function mark($row) {
1427 $num_correct = array_sum(array_slice($row, 3));
1429 if (is_array($_POST['answers'][$row['question_id']]) && count($_POST['answers'][$row['question_id']]) > 1) {
1430 if (($i = array_search('-1', $_POST['answers'][$row['question_id']])) !== FALSE) {
1431 unset($_POST['answers'][$row['question_id']][$i]);
1433 $num_answer_correct = 0;
1434 foreach ($_POST['answers'][$row['question_id']] as $item_id => $answer) {
1435 if ($row['answer_' . $answer]) {
1437 $num_answer_correct++;
1440 $num_answer_correct--;
1442 $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
1444 if ($num_answer_correct == $num_correct) {
1445 $score = $row['weight'];
1449 $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
1452 $_POST['answers'][$row['question_id']] = '-1'; // left blank
1458 //QTI Import multianswer Question
1459 function importQTI($_POST){
1461 // $_POST = $this->_POST;
1463 if ($_POST['question'] == ''){
1464 $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1467 //Multiple answer can have 0+ answers, in the QTIImport.class, if size(answer) < 2, answer will be came a scalar.
1468 //The following code will change $_POST[answer] back to a vector.
1469 $_POST['answer'] = $_POST['answers'];
1471 if (!$msg->containsErrors()) {
1472 $choice_new = array(); // stores the non-blank choices
1473 $answer_new = array(); // stores the associated "answer" for the choices
1475 foreach ($_POST['choice'] as $choiceNum=>$choiceOpt) {
1476 $choiceOpt = validate_length($choiceOpt, 255);
1477 $choiceOpt = trim($choiceOpt);
1478 $_POST['answer'][$choiceNum] = intval($_POST['answer'][$choiceNum]);
1479 if ($choiceOpt == '') {
1480 /* an empty option can't be correct */
1481 $_POST['answer'][$choiceNum] = 0;
1483 /* filter out empty choices/ remove gaps */
1484 $choice_new[] = $choiceOpt;
1485 if (in_array($choiceNum, $_POST['answer'])){
1491 if ($_POST['answer'][$choiceNum] != 0)
1496 if ($has_answer != TRUE) {
1498 $hidden_vars['required'] = htmlspecialchars($_POST['required']);
1499 $hidden_vars['feedback'] = htmlspecialchars($_POST['feedback']);
1500 $hidden_vars['question'] = htmlspecialchars($_POST['question']);
1501 $hidden_vars['category_id'] = htmlspecialchars($_POST['category_id']);
1503 for ($i = 0; $i < count($choice_new); $i++) {
1504 $hidden_vars['answer['.$i.']'] = htmlspecialchars($answer_new[$i]);
1505 $hidden_vars['choice['.$i.']'] = htmlspecialchars($choice_new[$i]);
1508 $msg->addConfirm('NO_ANSWER', $hidden_vars);
1510 //add slahes throughout - does that fix it?
1511 $_POST['answer'] = $answer_new;
1512 $_POST['choice'] = $choice_new;
1513 $_POST['answer'] = array_pad($_POST['answer'], 10, 0);
1514 $_POST['choice'] = array_pad($_POST['choice'], 10, '');
1516 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1517 // $_POST['question'] = $addslashes($_POST['question']);
1519 $sql_params = array( $_POST['category_id'],
1520 $_SESSION['course_id'],
1523 $_POST['choice'][0],
1524 $_POST['choice'][1],
1525 $_POST['choice'][2],
1526 $_POST['choice'][3],
1527 $_POST['choice'][4],
1528 $_POST['choice'][5],
1529 $_POST['choice'][6],
1530 $_POST['choice'][7],
1531 $_POST['choice'][8],
1532 $_POST['choice'][9],
1533 $_POST['answer'][0],
1534 $_POST['answer'][1],
1535 $_POST['answer'][2],
1536 $_POST['answer'][3],
1537 $_POST['answer'][4],
1538 $_POST['answer'][5],
1539 $_POST['answer'][6],
1540 $_POST['answer'][7],
1541 $_POST['answer'][8],
1542 $_POST['answer'][9]);
1544 $sql = vsprintf(AT_SQL_QUESTION_MULTIANSWER, $sql_params);
1546 $result = mysql_query($sql, $db);
1548 return mysql_insert_id();