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'];
303 //No questions in the test
304 if (sizeof($question_ids)==0){
308 $question_ids_delim = implode(',',$question_ids);
310 //$sql = "SELECT * FROM ".TABLE_PREFIX."tests_questions WHERE course_id=$_SESSION[course_id] AND question_id IN($question_ids_delim)";
311 $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";
313 $result = mysql_query($sql, $db);
314 while ($row = mysql_fetch_assoc($result)) {
315 $obj = TestQuestions::getQuestion($row['type']);
317 $local_xml = $obj->exportQTI($row, $course_language_charset, '1.2.1');
318 $local_dependencies = array();
320 $text_blob = implode(' ', $row);
321 $local_dependencies = get_html_resources($text_blob);
322 $dependencies = array_merge($dependencies, $local_dependencies);
324 $xml = $xml . "\n\n" . $local_xml;
327 //files that are found inside the test; used by print_organization(), to add all test files into QTI/ folder.
328 $test_files = $dependencies;
330 $resources[] = array('href' => 'tests_'.$tid.'.xml',
331 'dependencies' => array_keys($dependencies));
336 $sql = "SELECT title FROM ".TABLE_PREFIX."tests WHERE test_id = $tid";
337 $result = mysql_query($sql, $db);
338 $row = mysql_fetch_array($result);
340 //TODO: wrap around xml now
341 $savant->assign('xml_content', $xml);
342 $savant->assign('title', htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8'));
343 $xml = $savant->fetch('test_questions/wrapper.tmpl.php');
345 $xml_filename = 'tests_'.$tid.'.xml';
347 $zipfile->add_file($xml, $xml_filename);
349 $zipfile->add_file($xml, 'QTI/'.$xml_filename);
352 // add any dependency files:
354 foreach ($dependencies as $resource => $resource_server_path) {
355 //add this file in if it's not already in the zip package
356 if (!in_array($resource_server_path, $test_zipped_files)){
357 $zipfile->add_file(@file_get_contents($resource_server_path), 'resources/'.$resource, filemtime($resource_server_path));
358 $test_zipped_files[] = $resource_server_path;
364 // construct the manifest xml
365 $savant->assign('resources', $resources);
366 $savant->assign('dependencies', array_keys($dependencies));
367 $savant->assign('encoding', $course_language_charset);
368 $savant->assign('title', $test_title);
369 $savant->assign('xml_filename', $xml_filename);
371 $manifest_xml = $savant->fetch('test_questions/manifest_qti_1p2.tmpl.php');
372 $zipfile->add_file($manifest_xml, 'imsmanifest.xml');
376 $filename = str_replace(array(' ', ':'), '_', $_SESSION['course_title'].'-'.$test_title.'-'.date('Ymd'));
377 $zipfile->send_file($filename);
381 $return_array[$xml_filename] = array_keys($dependencies);
382 return $return_array;
388 * Recursively create folders
389 * For the purpose of this webapp only. All the paths are seperated by a /
390 * And thus this function will loop through each directory and create them on the way
391 * if it doesn't exist.
394 function recursive_mkdir($path, $mode = 0700) {
395 $dirs = explode(DIRECTORY_SEPARATOR , $path);
396 $count = count($dirs);
398 for ($i = 0; $i < $count; ++$i) {
399 $path .= $dirs[$i].DIRECTORY_SEPARATOR;
400 //If the directory has not been created, create it and return error on failure
401 if (!is_dir($path) && !mkdir($path, $mode)) {
410 * keeps count of the question number (when displaying the question)
411 * need this function because PHP 4 doesn't support static members
413 function TestQuestionCounter($increment = FALSE) {
416 if (!isset($count)) {
430 * Note that all PHP 5 OO declarations and signatures are commented out to be
431 * backwards compatible with PHP 4.
434 /*abstract */ class AbstractTestQuestion {
436 * Savant2 $savant - refrence to the savant obj
438 /*protected */ var $savant;
441 * Constructor method. Initialises variables.
443 function AbstractTestQuestion(&$savant) { $this->savant =& $savant; }
448 /*final public */function seed($salt) {
450 * by controlling the seed before calling array_rand() we insure that
451 * we can un-randomize the order for marking.
452 * used with ordering type questions only.
454 srand($salt + $_SESSION['member_id']);
460 /*final public */function unseed() {
461 // To fix http://www.atutor.ca/atutor/mantis/view.php?id=3167
462 // Disturb the seed for ordering questions after mark to avoid the deterioration
463 // of the random distribution due to a repeated initialization of the same random seed
464 list($usec, $sec) = explode(" ", microtime());
465 srand((int)($usec*10));
470 * Prints the name of this question
472 /*final public */function printName() { echo $this->getName(); }
476 * Prints the name of this question
478 /*final public */function getName() { return _AT($this->sNameVar); }
482 * Returns the prefix string (used for file names)
484 /*final public */function getPrefix() { return $this->sPrefix; }
487 * Display the current question (for taking or previewing a test/question)
489 /*final public */function display($row, $response = '') {
490 // print the generic question header
491 $this->displayHeader($row['weight']);
493 // print the question specific template
494 $row['question'] = format_content($row['question'], 1, '');
495 $this->assignDisplayVariables($row, $response);
496 $this->savant->display('test_questions/' . $this->sPrefix . '.tmpl.php');
498 // print the generic question footer
499 $this->displayFooter();
503 * Display the result for the current question
505 /*final public */function displayResult($row, $answer_row, $editable = FALSE) {
506 // print the generic question header
507 $this->displayHeader($row['weight'], $answer_row['score'], $editable ? $row['question_id'] : FALSE);
509 // print the question specific template
510 $this->assignDisplayResultVariables($row, $answer_row);
511 $this->savant->display('test_questions/' . $this->sPrefix . '_result.tmpl.php');
513 // print the generic question footer
514 $this->displayFooter();
519 * print the question template header
521 /*final public */function displayResultStatistics($row, $answers) {
522 TestQuestionCounter(TRUE);
523 $this->assignDisplayStatisticsVariables($row, $answers);
524 $this->savant->display('test_questions/' . $this->sPrefix . '_stats.tmpl.php');
527 /*final public */function exportQTI($row, $encoding, $version) {
528 $this->savant->assign('encoding', $encoding);
529 //Convert all row values to html entities
530 foreach ($row as $k=>$v){
531 $row[$k] = htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); //not using htmlentities cause it changes some languages falsely.
533 $this->assignQTIVariables($row);
534 if ($version=='2.1') {
535 $xml = $this->savant->fetch('test_questions/'. $this->sPrefix . '_qti_2p1.tmpl.php');
537 $xml = $this->savant->fetch('test_questions/'. $this->sPrefix . '_qti_1p2.tmpl.php');
543 * print the question template header
545 /*final private */function displayHeader($weight, $score = FALSE, $question_id = FALSE) {
546 TestQuestionCounter(TRUE);
548 if ($score) $score = intval($score);
549 $this->savant->assign('question_id', $question_id);
550 $this->savant->assign('score', $score);
551 $this->savant->assign('weight', $weight);
552 $this->savant->assign('type', _AT($this->sNameVar));
553 $this->savant->assign('number', TestQuestionCounter());
554 $this->savant->display('test_questions/header.tmpl.php');
558 * print the question template footer
560 /*final private */function displayFooter() {
561 $this->savant->display('test_questions/footer.tmpl.php');
565 * return only the non-empty choices from $row.
566 * assumes choices are sequential.
568 /*protected */function getChoices($row) {
570 for ($i=0; $i < 10; $i++) {
571 if ($row['choice_'.$i] != '') {
573 $choices[] = $row['choice_'.$i];
586 class OrderingQuestion extends AbstractTestQuestion {
587 /*protected */ var $sNameVar = 'test_ordering';
588 /*protected */ var $sPrefix = 'ordering';
590 /*protected */function assignDisplayResultVariables($row, $answer_row) {
591 $answers = explode('|', $answer_row['answer']);
593 $num_choices = count($this->getChoices($row));
595 $this->savant->assign('base_href', AT_BASE_HREF);
596 $this->savant->assign('num_choices', $num_choices);
597 $this->savant->assign('answers', $answers);
598 $this->savant->assign('row', $row);
601 /*protected */function assignQTIVariables($row) {
602 $choices = $this->getChoices($row);
603 $num_choices = count($choices);
605 $this->savant->assign('num_choices', $num_choices);
606 $this->savant->assign('row', $row);
609 /*protected */function assignDisplayVariables($row, $response) {
610 // determine the number of choices this question has
611 // and save those choices to be re-assigned back to $row
612 // in the randomized order.
613 $choices = $this->getChoices($row);
614 $num_choices = count($choices);
616 // response from the test_answers table is in the correct order
617 // so, they have to be re-randomized in the same order as the
618 // choices are. this is only possible because of the seed() method.
619 $response = explode('|', $response);
620 $new_response = array();
622 // randomize the order of choices and re-assign to $row
623 $this->seed($row['question_id']);
624 $rand = array_rand($choices, $num_choices);
625 for ($i=0; $i < 10; $i++) {
626 $row['choice_'.$i] = $choices[$rand[$i]];
627 $new_response[$i] = $response[$rand[$i]];
630 $this->savant->assign('num_choices', $num_choices);
631 $this->savant->assign('row', $row);
633 $this->savant->assign('response', $new_response);
636 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
638 foreach ($answers as $answer) {
639 $num_results += $answer['count'];
642 $choices = $this->getChoices($row);
643 $num_choices = count($choices);
645 $final_answers = array(); // assoc array of # of times that key was used correctly 0, 1, ... $num -1
646 foreach ($answers as $key => $value) {
647 $values = explode('|', $key);
648 // we assume $values is never empty and contains $num number of answers
649 for ($i=0; $i<=$num_choices; $i++) {
650 if ($values[$i] == $i) {
651 $final_answers[$i] += $answers[$key]['count'];
656 $this->savant->assign('num_results', $num_results);
657 $this->savant->assign('num_choices', $num_choices);
658 $this->savant->assign('answers', $final_answers);
659 $this->savant->assign('row', $row);
662 /*public */function mark($row) {
663 $this->seed($row['question_id']);
664 $num_choices = count($_POST['answers'][$row['question_id']]);
665 $answers = range(0, $num_choices-1);
666 $answers = array_rand($answers, $num_choices);
668 // Disturb the seed for ordering questions after mark to avoid the deterioration
669 // of the random distribution due to a repeated initialization of the same random seed
672 $num_answer_correct = 0;
674 $ordered_answers = array();
676 for ($i = 0; $i < $num_choices ; $i++) {
677 $_POST['answers'][$row['question_id']][$i] = intval($_POST['answers'][$row['question_id']][$i]);
679 if ($_POST['answers'][$row['question_id']][$i] == -1) {
680 // nothing to do. it was left blank
681 } else if ($_POST['answers'][$row['question_id']][$i] == $answers[$i]) {
682 $num_answer_correct++;
684 $ordered_answers[$answers[$i]] = $_POST['answers'][$row['question_id']][$i];
686 ksort($ordered_answers);
690 // to avoid roundoff errors:
691 if ($num_answer_correct == $num_choices) {
692 $score = $row['weight'];
693 } else if ($num_answer_correct > 0) {
694 $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
695 if ( (float) (int) $score == $score) {
696 $score = (int) $score; // a whole number with decimals, eg. "2.00"
698 $score = trim($score, '0'); // remove trailing zeros, if any, eg. "2.50"
702 $_POST['answers'][$row['question_id']] = implode('|', $ordered_answers);
707 //QTI Import Ordering Question
708 function importQTI($_POST){
711 if ($_POST['question'] == ''){
712 $missing_fields[] = _AT('question');
715 if (trim($_POST['choice'][0]) == '') {
716 $missing_fields[] = _AT('item').' 1';
718 if (trim($_POST['choice'][1]) == '') {
719 $missing_fields[] = _AT('item').' 2';
722 if ($missing_fields) {
723 $missing_fields = implode(', ', $missing_fields);
724 $msg->addError(array('EMPTY_FIELDS', $missing_fields));
727 if (!$msg->containsErrors()) {
728 $choice_new = array(); // stores the non-blank choices
729 $answer_new = array(); // stores the non-blank answers
730 $order = 0; // order count
731 for ($i=0; $i<10; $i++) {
733 * Db defined it to be 255 length, chop strings off it it's less than that
736 $_POST['choice'][$i] = validate_length($_POST['choice'][$i], 255);
737 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
739 if ($_POST['choice'][$i] != '') {
740 /* filter out empty choices/ remove gaps */
741 $choice_new[] = $_POST['choice'][$i];
742 $answer_new[] = $order++;
746 $_POST['choice'] = array_pad($choice_new, 10, '');
747 $answer_new = array_pad($answer_new, 10, 0);
748 // $_POST['feedback'] = $addslashes($_POST['feedback']);
749 // $_POST['question'] = $addslashes($_POST['question']);
751 $sql_params = array( $_POST['category_id'],
752 $_SESSION['course_id'],
776 $sql = vsprintf(AT_SQL_QUESTION_ORDERING, $sql_params);
778 $result = mysql_query($sql, $db);
780 return mysql_insert_id();
790 class TruefalseQuestion extends AbstracttestQuestion {
791 /*protected */ var $sPrefix = 'truefalse';
792 /*protected */ var $sNameVar = 'test_tf';
794 /*protected */function assignQTIVariables($row) {
795 $this->savant->assign('row', $row);
798 /*protected */function assignDisplayResultVariables($row, $answer_row) {
800 $this->savant->assign('base_href', AT_BASE_HREF);
801 $this->savant->assign('answers', $answer_row['answer']);
802 $this->savant->assign('row', $row);
805 /*protected */function assignDisplayVariables($row, $response) {
806 $this->savant->assign('row', $row);
807 $this->savant->assign('response', $response);
810 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
812 foreach ($answers as $answer) {
813 $num_results += $answer['count'];
816 $this->savant->assign('num_results', $num_results);
817 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
818 $this->savant->assign('num_true', (int) $answers['1']['count']);
819 $this->savant->assign('num_false', (int) $answers['2']['count']);
820 $this->savant->assign('row', $row);
823 /*public */function mark($row) {
824 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
826 if ($row['answer_0'] == $_POST['answers'][$row['question_id']]) {
827 return (int) $row['weight'];
832 //QTI Import True/False Question
833 function importQTI($_POST){
836 if ($_POST['question'] == ''){
837 $msg->addError(array('EMPTY_FIELDS', _AT('statement')));
840 //assign true answer to 1, false answer to 2, idk to 3, for ATutor
841 if ($_POST['answer'] == 'ChoiceT'){
842 $_POST['answer'] = 1;
844 $_POST['answer'] = 2;
847 if (!$msg->containsErrors()) {
848 // $_POST['feedback'] = $addslashes($_POST['feedback']);
849 // $_POST['question'] = $addslashes($_POST['question']);
852 $sql_params = array( $_POST['category_id'],
853 $_SESSION['course_id'],
858 $sql = vsprintf(AT_SQL_QUESTION_TRUEFALSE, $sql_params);
859 $result = mysql_query($sql, $db);
861 return mysql_insert_id();
871 class LikertQuestion extends AbstracttestQuestion {
872 /*protected */ var $sPrefix = 'likert';
873 /*protected */ var $sNameVar = 'test_lk';
875 /*protected */function assignQTIVariables($row) {
876 $choices = $this->getChoices($row);
877 $num_choices = count($choices);
879 $this->savant->assign('num_choices', $num_choices);
880 $this->savant->assign('row', $row);
883 /*protected */function assignDisplayResultVariables($row, $answer_row) {
884 $this->savant->assign('answer', $answer_row['answer']);
885 $this->savant->assign('row', $row);
888 /*protected */function assignDisplayVariables($row, $response) {
889 $choices = $this->getChoices($row);
890 $num_choices = count($choices);
892 $this->savant->assign('num_choices', $num_choices);
893 $this->savant->assign('row', $row);
895 if (empty($response)) {
898 $this->savant->assign('response', $response);
901 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
903 foreach ($answers as $answer) {
904 $num_results += $answer['count'];
907 $choices = $this->getChoices($row);
908 $num_choices = count($choices);
911 for ($i=0; $i<$num_choices; $i++) {
912 $sum += ($i+1) * $answers[$i]['count'];
914 $average = round($sum/$num_results, 1);
916 $this->savant->assign('num_results', $num_results);
917 $this->savant->assign('average', $average);
918 $this->savant->assign('num_choices', $num_choices);
919 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
920 $this->savant->assign('answers', $answers);
921 $this->savant->assign('row', $row);
924 /*public */function mark($row) {
925 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
929 //QTI Import Likert Question
930 function importQTI($_POST){
932 // $_POST = $this->_POST;
934 $empty_fields = array();
935 if ($_POST['question'] == ''){
936 $empty_fields[] = _AT('question');
938 if ($_POST['choice'][0] == '') {
939 $empty_fields[] = _AT('choice').' 1';
942 if ($_POST['choice'][1] == '') {
943 $empty_fields[] = _AT('choice').' 2';
946 if (!empty($empty_fields)) {
947 // $msg->addError(array('EMPTY_FIELDS', implode(', ', $empty_fields)));
950 if (!$msg->containsErrors()) {
951 $_POST['feedback'] = '';
952 // $_POST['question'] = $addslashes($_POST['question']);
954 for ($i=0; $i<10; $i++) {
955 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
956 $_POST['answer'][$i] = intval($_POST['answer'][$i]);
958 if ($_POST['choice'][$i] == '') {
959 /* an empty option can't be correct */
960 $_POST['answer'][$i] = 0;
964 $sql_params = array( $_POST['category_id'],
965 $_SESSION['course_id'],
987 $_POST['answer'][9]);
989 $sql = vsprintf(AT_SQL_QUESTION_LIKERT, $sql_params);
990 $result = mysql_query($sql, $db);
992 return mysql_insert_id();
1002 class LongQuestion extends AbstracttestQuestion {
1003 /*protected */ var $sPrefix = 'long';
1004 /*protected */ var $sNameVar = 'test_open';
1006 /*protected */function assignQTIVariables($row) {
1007 $this->savant->assign('row', $row);
1010 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1011 $this->savant->assign('answer', $answer_row['answer']);
1012 $this->savant->assign('row', $row);
1015 /*protected */function assignDisplayVariables($row, $response) {
1016 $this->savant->assign('row', $row);
1017 $this->savant->assign('response', $response);
1020 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1022 foreach ($answers as $answer) {
1023 $num_results += $answer['count'];
1026 $this->savant->assign('num_results', $num_results);
1027 $this->savant->assign('num_blanks', (int) $answers['']['count']);
1028 $this->savant->assign('answers', $answers);
1029 $this->savant->assign('row', $row);
1032 /*public */function mark($row) {
1034 $_POST['answers'][$row['question_id']] = $addslashes($_POST['answers'][$row['question_id']]);
1038 //QTI Import Open end/long Question
1039 function importQTI($_POST){
1041 // $_POST = $this->_POST;
1043 if ($_POST['question'] == ''){
1044 // $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1047 if (!$msg->containsErrors()) {
1048 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1049 // $_POST['question'] = $addslashes($_POST['question']);
1051 if ($_POST['property']==''){
1052 $_POST['property'] = 4; //essay
1055 $sql_params = array( $_POST['category_id'],
1056 $_SESSION['course_id'],
1059 $_POST['property']);
1061 $sql = vsprintf(AT_SQL_QUESTION_LONG, $sql_params);
1063 $result = mysql_query($sql, $db);
1065 return mysql_insert_id();
1075 class MatchingQuestion extends AbstracttestQuestion {
1076 /*protected */ var $sPrefix = 'matching';
1077 /*protected */ var $sNameVar = 'test_matching';
1079 /*protected */function assignQTIVariables($row) {
1080 $choices = $this->getChoices($row);
1081 $num_choices = count($choices);
1084 for ($i=0; $i < 10; $i++) {
1085 if ($row['option_'. $i] != '') {
1090 $this->savant->assign('num_choices', $num_choices);
1091 $this->savant->assign('num_options', $num_options);
1092 $this->savant->assign('row', $row);
1095 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1097 for ($i=0; $i < 10; $i++) {
1098 if ($row['option_'. $i] != '') {
1103 $answer_row['answer'] = explode('|', $answer_row['answer']);
1107 $this->savant->assign('base_href', AT_BASE_HREF);
1108 $this->savant->assign('answers', $answer_row['answer']);
1109 $this->savant->assign('letters', $_letters);
1110 $this->savant->assign('num_options', $num_options);
1111 $this->savant->assign('row', $row);
1114 /*protected */function assignDisplayVariables($row, $response) {
1115 $choices = $this->getChoices($row);
1116 $num_choices = count($choices);
1118 if (empty($response)) {
1119 $response = array_fill(0, $num_choices, -1);
1121 $response = explode('|', $response);
1125 for ($i=0; $i < 10; $i++) {
1126 if ($row['option_'. $i] != '') {
1133 $this->savant->assign('num_choices', $num_choices);
1134 $this->savant->assign('base_href', AT_BASE_HREF);
1135 $this->savant->assign('letters', $_letters);
1136 $this->savant->assign('num_options', $num_options);
1137 $this->savant->assign('row', $row);
1139 $this->savant->assign('response', $response);
1142 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1143 $choices = $this->getChoices($row);
1144 $num_choices = count($choices);
1147 foreach ($answers as $answer) {
1148 $num_results += $answer['count'];
1151 foreach ($answers as $key => $value) {
1152 $values = explode('|', $key);
1153 if (count($values) > 1) {
1154 for ($i=0; $i<count($values); $i++) {
1155 $answers[$values[$i]]['count']++;
1160 $this->savant->assign('num_choices', $num_choices);
1161 $this->savant->assign('num_results', $num_results);
1162 $this->savant->assign('answers', $answers);
1163 $this->savant->assign('row', $row);
1166 /*public */function mark($row) {
1167 $num_choices = count($_POST['answers'][$row['question_id']]);
1168 $num_answer_correct = 0;
1169 foreach ($_POST['answers'][$row['question_id']] as $item_id => $response) {
1170 if ($row['answer_' . $item_id] == $response) {
1171 $num_answer_correct++;
1173 $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
1177 // to avoid roundoff errors:
1178 if ($num_answer_correct == $num_choices) {
1179 $score = $row['weight'];
1180 } else if ($num_answer_correct > 0) {
1181 $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
1182 if ( (float) (int) $score == $score) {
1183 $score = (int) $score; // a whole number with decimals, eg. "2.00"
1185 $score = trim($score, '0'); // remove trailing zeros, if any
1189 $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
1194 //QTI Import Matching Question
1195 function importQTI($_POST){
1197 // $_POST = $this->_POST;
1199 if (!is_array($_POST['answer'])){
1200 $temp = $_POST['answer'];
1201 $_POST['answer'] = array();
1202 $_POST['answer'][0] = $temp;
1204 ksort($_POST['answer']); //array_pad returns an array disregard of the array keys
1205 //default for matching is '-'
1206 $_POST['answer']= array_pad($_POST['answer'], 10, -1);
1208 for ($i = 0 ; $i < 10; $i++) {
1209 $_POST['groups'][$i] = trim($_POST['groups'][$i]);
1210 $_POST['answer'][$i] = (int) $_POST['answer'][$i];
1211 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
1214 if (!$_POST['groups'][0]
1215 || !$_POST['groups'][1]
1216 || !$_POST['choice'][0]
1217 || !$_POST['choice'][1]) {
1218 // $msg->addError('QUESTION_EMPTY');
1221 if (!$msg->containsErrors()) {
1222 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1223 // $_POST['instructions'] = $addslashes($_POST['instructions']);
1225 $sql_params = array( $_POST['category_id'],
1226 $_SESSION['course_id'],
1229 $_POST['groups'][0],
1230 $_POST['groups'][1],
1231 $_POST['groups'][2],
1232 $_POST['groups'][3],
1233 $_POST['groups'][4],
1234 $_POST['groups'][5],
1235 $_POST['groups'][6],
1236 $_POST['groups'][7],
1237 $_POST['groups'][8],
1238 $_POST['groups'][9],
1239 $_POST['answer'][0],
1240 $_POST['answer'][1],
1241 $_POST['answer'][2],
1242 $_POST['answer'][3],
1243 $_POST['answer'][4],
1244 $_POST['answer'][5],
1245 $_POST['answer'][6],
1246 $_POST['answer'][7],
1247 $_POST['answer'][8],
1248 $_POST['answer'][9],
1249 $_POST['choice'][0],
1250 $_POST['choice'][1],
1251 $_POST['choice'][2],
1252 $_POST['choice'][3],
1253 $_POST['choice'][4],
1254 $_POST['choice'][5],
1255 $_POST['choice'][6],
1256 $_POST['choice'][7],
1257 $_POST['choice'][8],
1258 $_POST['choice'][9]);
1260 $sql = vsprintf(AT_SQL_QUESTION_MATCHINGDD, $sql_params);
1262 $result = mysql_query($sql, $db);
1264 return mysql_insert_id();
1271 * matchingddQuestion
1274 class MatchingddQuestion extends MatchingQuestion {
1275 /*protected */ var $sPrefix = 'matchingdd';
1276 /*protected */ var $sNameVar = 'test_matchingdd';
1280 * multichoiceQuestion
1283 class MultichoiceQuestion extends AbstracttestQuestion {
1284 /*protected */ var $sPrefix = 'multichoice';
1285 /*protected */var $sNameVar = 'test_mc';
1287 /*protected */function assignQTIVariables($row) {
1288 $choices = $this->getChoices($row);
1289 $num_choices = count($choices);
1291 $this->savant->assign('num_choices', $num_choices);
1292 $this->savant->assign('row', $row);
1295 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1296 if (strpos($answer_row['answer'], '|') !== false) {
1297 $answer_row['answer'] = explode('|', $answer_row['answer']);
1299 $answer_row['answer'] = array($answer_row['answer']);
1302 $this->savant->assign('base_href', AT_BASE_HREF);
1303 $this->savant->assign('answers', $answer_row['answer']);
1304 $this->savant->assign('row', $row);
1307 /*protected */function assignDisplayVariables($row, $response) {
1308 $choices = $this->getChoices($row);
1309 $num_choices = count($choices);
1311 if ($response == '') {
1314 $response = explode('|', $response);
1315 $this->savant->assign('response', $response);
1317 $this->savant->assign('num_choices', $num_choices);
1318 $this->savant->assign('row', $row);
1321 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1322 $choices = $this->getChoices($row);
1323 $num_choices = count($choices);
1326 foreach ($answers as $answer) {
1327 $num_results += $answer['count'];
1330 foreach ($answers as $key => $value) {
1331 $values = explode('|', $key);
1332 if (count($values) > 1) {
1333 for ($i=0; $i<count($values); $i++) {
1334 $answers[$values[$i]]['count']++;
1339 $this->savant->assign('num_choices', $num_choices);
1340 $this->savant->assign('num_results', $num_results);
1341 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
1342 $this->savant->assign('answers', $answers);
1343 $this->savant->assign('row', $row);
1346 /*public */function mark($row) {
1348 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
1349 if ($row['answer_' . $_POST['answers'][$row['question_id']]]) {
1350 $score = $row['weight'];
1351 } else if ($_POST['answers'][$row['question_id']] == -1) {
1353 for($i=0; $i<10; $i++) {
1354 $has_answer += $row['answer_'.$i];
1356 if (!$has_answer && $row['weight']) {
1357 // If MC has no answer and user answered "leave blank"
1358 $score = $row['weight'];
1364 //QTI Import Multiple Choice Question
1365 function importQTI($_POST){
1367 // $_POST = $this->_POST;
1368 if ($_POST['question'] == ''){
1369 $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1372 if (!$msg->containsErrors()) {
1373 // $_POST['question'] = $addslashes($_POST['question']);
1375 for ($i=0; $i<10; $i++) {
1376 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
1379 $answers = array_fill(0, 10, 0);
1380 if (is_array($_POST['answer'])){
1381 $answers[0] = 1; //default the first to be the right answer. TODO, use summation of points.
1383 $answers[$_POST['answer']] = 1;
1386 $sql_params = array( $_POST['category_id'],
1387 $_SESSION['course_id'],
1390 $_POST['choice'][0],
1391 $_POST['choice'][1],
1392 $_POST['choice'][2],
1393 $_POST['choice'][3],
1394 $_POST['choice'][4],
1395 $_POST['choice'][5],
1396 $_POST['choice'][6],
1397 $_POST['choice'][7],
1398 $_POST['choice'][8],
1399 $_POST['choice'][9],
1411 $sql = vsprintf(AT_SQL_QUESTION_MULTI, $sql_params);
1412 $result = mysql_query($sql, $db);
1414 return mysql_insert_id();
1421 * multianswerQuestion
1424 class MultianswerQuestion extends MultichoiceQuestion {
1425 /*protected */ var $sPrefix = 'multianswer';
1426 /*protected */ var $sNameVar = 'test_ma';
1428 /*public */function mark($row) {
1429 $num_correct = array_sum(array_slice($row, 3));
1431 if (is_array($_POST['answers'][$row['question_id']]) && count($_POST['answers'][$row['question_id']]) > 1) {
1432 if (($i = array_search('-1', $_POST['answers'][$row['question_id']])) !== FALSE) {
1433 unset($_POST['answers'][$row['question_id']][$i]);
1435 $num_answer_correct = 0;
1436 foreach ($_POST['answers'][$row['question_id']] as $item_id => $answer) {
1437 if ($row['answer_' . $answer]) {
1439 $num_answer_correct++;
1442 $num_answer_correct--;
1444 $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
1446 if ($num_answer_correct == $num_correct) {
1447 $score = $row['weight'];
1451 $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
1454 $_POST['answers'][$row['question_id']] = '-1'; // left blank
1460 //QTI Import multianswer Question
1461 function importQTI($_POST){
1463 // $_POST = $this->_POST;
1465 if ($_POST['question'] == ''){
1466 $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1469 //Multiple answer can have 0+ answers, in the QTIImport.class, if size(answer) < 2, answer will be came a scalar.
1470 //The following code will change $_POST[answer] back to a vector.
1471 $_POST['answer'] = $_POST['answers'];
1473 if (!$msg->containsErrors()) {
1474 $choice_new = array(); // stores the non-blank choices
1475 $answer_new = array(); // stores the associated "answer" for the choices
1477 foreach ($_POST['choice'] as $choiceNum=>$choiceOpt) {
1478 $choiceOpt = validate_length($choiceOpt, 255);
1479 $choiceOpt = trim($choiceOpt);
1480 $_POST['answer'][$choiceNum] = intval($_POST['answer'][$choiceNum]);
1481 if ($choiceOpt == '') {
1482 /* an empty option can't be correct */
1483 $_POST['answer'][$choiceNum] = 0;
1485 /* filter out empty choices/ remove gaps */
1486 $choice_new[] = $choiceOpt;
1487 if (in_array($choiceNum, $_POST['answer'])){
1493 if ($_POST['answer'][$choiceNum] != 0)
1498 if ($has_answer != TRUE) {
1500 $hidden_vars['required'] = htmlspecialchars($_POST['required']);
1501 $hidden_vars['feedback'] = htmlspecialchars($_POST['feedback']);
1502 $hidden_vars['question'] = htmlspecialchars($_POST['question']);
1503 $hidden_vars['category_id'] = htmlspecialchars($_POST['category_id']);
1505 for ($i = 0; $i < count($choice_new); $i++) {
1506 $hidden_vars['answer['.$i.']'] = htmlspecialchars($answer_new[$i]);
1507 $hidden_vars['choice['.$i.']'] = htmlspecialchars($choice_new[$i]);
1510 $msg->addConfirm('NO_ANSWER', $hidden_vars);
1512 //add slahes throughout - does that fix it?
1513 $_POST['answer'] = $answer_new;
1514 $_POST['choice'] = $choice_new;
1515 $_POST['answer'] = array_pad($_POST['answer'], 10, 0);
1516 $_POST['choice'] = array_pad($_POST['choice'], 10, '');
1518 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1519 // $_POST['question'] = $addslashes($_POST['question']);
1521 $sql_params = array( $_POST['category_id'],
1522 $_SESSION['course_id'],
1525 $_POST['choice'][0],
1526 $_POST['choice'][1],
1527 $_POST['choice'][2],
1528 $_POST['choice'][3],
1529 $_POST['choice'][4],
1530 $_POST['choice'][5],
1531 $_POST['choice'][6],
1532 $_POST['choice'][7],
1533 $_POST['choice'][8],
1534 $_POST['choice'][9],
1535 $_POST['answer'][0],
1536 $_POST['answer'][1],
1537 $_POST['answer'][2],
1538 $_POST['answer'][3],
1539 $_POST['answer'][4],
1540 $_POST['answer'][5],
1541 $_POST['answer'][6],
1542 $_POST['answer'][7],
1543 $_POST['answer'][8],
1544 $_POST['answer'][9]);
1546 $sql = vsprintf(AT_SQL_QUESTION_MULTIANSWER, $sql_params);
1548 $result = mysql_query($sql, $db);
1550 return mysql_insert_id();