2 /****************************************************************/
4 /****************************************************************/
5 /* Copyright (c) 2002-2010 */
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 /****************************************************************/
14 require(AT_INCLUDE_PATH.'../mods/_standard/tests/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_id = $_SESSION['course_id'];
270 $course_title = $_SESSION['course_title'];
271 $course_language = $system_courses[$_SESSION['course_id']]['primary_language'];
273 if ($course_language == '') { // when oauth export into Transformable
274 $sql = "SELECT course_id, title, primary_language FROM ".TABLE_PREFIX."courses
275 WHERE course_id = (SELECT course_id FROM ".TABLE_PREFIX."tests
276 WHERE test_id=".$tid.")";
277 $result = mysql_query($sql, $db);
278 $course_row = mysql_fetch_assoc($result);
280 $course_language = $course_row['primary_language'];
281 $course_id = $course_row['course_id'];
282 $course_title = $course_row['title'];
284 $courseLanguage =& $languageManager->getLanguage($course_language);
285 $course_language_charset = $courseLanguage->getCharacterSet();
293 if ($test_zipped_files == null){
294 $test_zipped_files = array();
298 $zipfile = new zipfile();
299 $zipfile->create_dir('resources/'); // for all the dependency files
301 $resources = array();
302 $dependencies = array();
304 // don't want to sort it, i want the same order out.
305 // asort($question_ids);
307 //TODO: Merge the following 2 sqls together.
308 //Randomized or not, export all the questions that are associated with it.
309 $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=$course_id AND TQA.test_id=$tid ORDER BY TQA.ordering, TQA.question_id";
310 $result = mysql_query($sql, $db);
311 $question_ids = array();
313 while (($question_row = mysql_fetch_assoc($result)) != false){
314 $question_ids[] = $question_row['question_id'];
317 //No questions in the test
318 if (sizeof($question_ids)==0){
322 $question_ids_delim = implode(',',$question_ids);
324 //$sql = "SELECT * FROM ".TABLE_PREFIX."tests_questions WHERE course_id=$_SESSION[course_id] AND question_id IN($question_ids_delim)";
325 $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";
327 $result = mysql_query($sql, $db);
328 while ($row = mysql_fetch_assoc($result)) {
329 $obj = TestQuestions::getQuestion($row['type']);
331 $local_xml = $obj->exportQTI($row, $course_language_charset, '1.2.1');
332 $local_dependencies = array();
334 $text_blob = implode(' ', $row);
335 $local_dependencies = get_html_resources($text_blob, $course_id);
336 $dependencies = array_merge($dependencies, $local_dependencies);
338 $xml = $xml . "\n\n" . $local_xml;
341 //files that are found inside the test; used by print_organization(), to add all test files into QTI/ folder.
342 $test_files = $dependencies;
344 $resources[] = array('href' => 'tests_'.$tid.'.xml',
345 'dependencies' => array_keys($dependencies));
350 $sql = "SELECT title, num_takes FROM ".TABLE_PREFIX."tests WHERE test_id = $tid";
351 $result = mysql_query($sql, $db);
352 $row = mysql_fetch_array($result);
354 //TODO: wrap around xml now
355 $savant->assign('xml_content', $xml);
356 $savant->assign('title', htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8'));
357 $savant->assign('num_takes', $row['num_takes']);
358 $savant->assign('use_cc', $use_cc);
359 $xml = $savant->fetch('test_questions/wrapper.tmpl.php');
361 $xml_filename = 'tests_'.$tid.'.xml';
363 $zipfile->add_file($xml, $xml_filename);
365 $zipfile->add_file($xml, 'QTI/'.$xml_filename);
368 // add any dependency files:
370 foreach ($dependencies as $resource => $resource_server_path) {
371 //add this file in if it's not already in the zip package
372 if (!in_array($resource_server_path, $test_zipped_files)){
373 $zipfile->add_file(@file_get_contents($resource_server_path), 'resources/'.$resource, filemtime($resource_server_path));
374 $test_zipped_files[] = $resource_server_path;
380 // construct the manifest xml
381 $savant->assign('resources', $resources);
382 $savant->assign('dependencies', array_keys($dependencies));
383 $savant->assign('encoding', $course_language_charset);
384 $savant->assign('title', $test_title);
385 $savant->assign('xml_filename', $xml_filename);
387 $manifest_xml = $savant->fetch('test_questions/manifest_qti_1p2.tmpl.php');
388 $zipfile->add_file($manifest_xml, 'imsmanifest.xml');
392 $filename = str_replace(array(' ', ':'), '_', $course_title.'-'.$test_title.'-'.date('Ymd'));
393 $zipfile->send_file($filename);
397 $return_array[$xml_filename] = array_keys($dependencies);
398 return $return_array;
404 * Recursively create folders
405 * For the purpose of this webapp only. All the paths are seperated by a /
406 * And thus this function will loop through each directory and create them on the way
407 * if it doesn't exist.
410 function recursive_mkdir($path, $mode = 0700) {
411 $dirs = explode(DIRECTORY_SEPARATOR , $path);
412 $count = count($dirs);
414 for ($i = 0; $i < $count; ++$i) {
415 $path .= $dirs[$i].DIRECTORY_SEPARATOR;
416 //If the directory has not been created, create it and return error on failure
417 if (!is_dir($path) && !mkdir($path, $mode)) {
426 * keeps count of the question number (when displaying the question)
427 * need this function because PHP 4 doesn't support static members
429 function TestQuestionCounter($increment = FALSE) {
432 if (!isset($count)) {
446 * Note that all PHP 5 OO declarations and signatures are commented out to be
447 * backwards compatible with PHP 4.
450 /*abstract */ class AbstractTestQuestion {
452 * Savant2 $savant - refrence to the savant obj
454 /*protected */ var $savant;
457 * Constructor method. Initialises variables.
459 function AbstractTestQuestion(&$savant) { $this->savant =& $savant; }
464 /*final public */function seed($salt) {
466 * by controlling the seed before calling array_rand() we insure that
467 * we can un-randomize the order for marking.
468 * used with ordering type questions only.
470 srand($salt + $_SESSION['member_id']);
476 /*final public */function unseed() {
477 // To fix http://www.atutor.ca/atutor/mantis/view.php?id=3167
478 // Disturb the seed for ordering questions after mark to avoid the deterioration
479 // of the random distribution due to a repeated initialization of the same random seed
480 list($usec, $sec) = explode(" ", microtime());
481 srand((int)($usec*10));
486 * Prints the name of this question
488 /*final public */function printName() { echo $this->getName(); }
492 * Prints the name of this question
494 /*final public */function getName() { return _AT($this->sNameVar); }
498 * Returns the prefix string (used for file names)
500 /*final public */function getPrefix() { return $this->sPrefix; }
503 * Display the current question (for taking or previewing a test/question)
505 /*final public */function display($row, $response = '') {
506 // print the generic question header
507 $this->displayHeader($row['weight']);
509 // print the question specific template
510 $row['question'] = format_content($row['question'], 1, '');
511 $this->assignDisplayVariables($row, $response);
512 $this->savant->display('test_questions/' . $this->sPrefix . '.tmpl.php');
514 // print the generic question footer
515 $this->displayFooter();
519 * Display the result for the current question
521 /*final public */function displayResult($row, $answer_row, $editable = FALSE) {
522 // print the generic question header
523 $this->displayHeader($row['weight'], $answer_row['score'], $editable ? $row['question_id'] : FALSE);
525 // print the question specific template
526 $this->assignDisplayResultVariables($row, $answer_row);
527 $this->savant->display('test_questions/' . $this->sPrefix . '_result.tmpl.php');
529 // print the generic question footer
530 $this->displayFooter();
535 * print the question template header
537 /*final public */function displayResultStatistics($row, $answers) {
538 TestQuestionCounter(TRUE);
539 $this->assignDisplayStatisticsVariables($row, $answers);
540 $this->savant->display('test_questions/' . $this->sPrefix . '_stats.tmpl.php');
543 /*final public */function exportQTI($row, $encoding, $version) {
544 $this->savant->assign('encoding', $encoding);
545 $this->savant->assign('weight', $row['weight']);
546 //Convert all row values to html entities
547 foreach ($row as $k=>$v){
548 $row[$k] = htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); //not using htmlentities cause it changes some languages falsely.
550 $this->assignQTIVariables($row);
551 if ($version=='2.1') {
552 $xml = $this->savant->fetch('test_questions/'. $this->sPrefix . '_qti_2p1.tmpl.php');
554 $xml = $this->savant->fetch('test_questions/'. $this->sPrefix . '_qti_1p2.tmpl.php');
560 * print the question template header
562 /*final private */function displayHeader($weight, $score = FALSE, $question_id = FALSE) {
563 TestQuestionCounter(TRUE);
565 if ($score) $score = intval($score);
566 $this->savant->assign('question_id', $question_id);
567 $this->savant->assign('score', $score);
568 $this->savant->assign('weight', $weight);
569 $this->savant->assign('type', _AT($this->sNameVar));
570 $this->savant->assign('number', TestQuestionCounter());
571 $this->savant->display('test_questions/header.tmpl.php');
575 * print the question template footer
577 /*final private */function displayFooter() {
578 $this->savant->display('test_questions/footer.tmpl.php');
582 * return only the non-empty choices from $row.
583 * assumes choices are sequential.
585 /*protected */function getChoices($row) {
587 for ($i=0; $i < 10; $i++) {
588 if ($row['choice_'.$i] != '') {
590 $choices[] = $row['choice_'.$i];
603 class OrderingQuestion extends AbstractTestQuestion {
604 /*protected */ var $sNameVar = 'test_ordering';
605 /*protected */ var $sPrefix = 'ordering';
607 /*protected */function assignDisplayResultVariables($row, $answer_row) {
608 $answers = explode('|', $answer_row['answer']);
610 $choices = $this->getChoices($row);
611 $num_choices = count($choices);
613 // randomize the order of choices and re-assign to $row
614 srand($row['question_id']);
616 srand($row['question_id']);
619 // generate and shuffle the right answer
620 $right_answers = range(0, $num_choices-1);
621 srand($row['question_id']);
622 shuffle($right_answers);
624 for ($i = 0; $i < count($choices); $i++) {
625 $row['choice_'.$i] = $choices[$i];
628 $this->savant->assign('base_href', AT_BASE_HREF);
629 $this->savant->assign('num_choices', $num_choices);
630 $this->savant->assign('answers', $answers);
631 $this->savant->assign('right_answers', $right_answers);
632 $this->savant->assign('row', $row);
635 /*protected */function assignQTIVariables($row) {
636 $choices = $this->getChoices($row);
637 $num_choices = count($choices);
639 $this->savant->assign('num_choices', $num_choices);
640 $this->savant->assign('row', $row);
643 /*protected */function assignDisplayVariables($row, $response) {
644 // determine the number of choices this question has
645 // and save those choices to be re-assigned back to $row
646 // in the randomized order.
647 $choices = $this->getChoices($row);
648 $num_choices = count($choices);
650 // response from the test_answers table is in the correct order
651 // so, they have to be re-randomized in the same order as the
652 // choices are. this is only possible because of the seed() method.
653 $response = explode('|', $response);
654 $new_response = array();
656 // randomize the order of choices and re-assign to $row
657 srand($row['question_id']);
660 srand($row['question_id']);
662 for ($i=0; $i < 10; $i++) {
663 $row['choice_'.$i] = $choices[$i];
666 $this->savant->assign('num_choices', $num_choices);
667 $this->savant->assign('row', $row);
668 $this->savant->assign('response', $response);
671 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
673 foreach ($answers as $answer) {
674 $num_results += $answer['count'];
677 $choices = $this->getChoices($row);
678 $num_choices = count($choices);
680 $final_answers = array(); // assoc array of # of times that key was used correctly 0, 1, ... $num -1
681 foreach ($answers as $key => $value) {
682 $values = explode('|', $key);
683 // we assume $values is never empty and contains $num number of answers
684 for ($i=0; $i<=$num_choices; $i++) {
685 if ($values[$i] == $i) {
686 $final_answers[$i] += $answers[$key]['count'];
691 $this->savant->assign('num_results', $num_results);
692 $this->savant->assign('num_choices', $num_choices);
693 $this->savant->assign('answers', $final_answers);
694 $this->savant->assign('row', $row);
697 /*public */function mark($row) {
698 $num_choices = count($_POST['answers'][$row['question_id']]);
699 $answers = range(0, $num_choices-1);
700 srand($row['question_id']);
703 // Disturb the seed for ordering questions after mark to avoid the deterioration
704 // of the random distribution due to a repeated initialization of the same random seed
707 $num_answer_correct = 0;
709 $ordered_answers = array();
711 for ($i = 0; $i < $num_choices ; $i++) {
712 $_POST['answers'][$row['question_id']][$i] = intval($_POST['answers'][$row['question_id']][$i]);
714 if ($_POST['answers'][$row['question_id']][$i] == -1) {
715 // nothing to do. it was left blank
716 } else if ($_POST['answers'][$row['question_id']][$i] == $answers[$i]) {
717 $num_answer_correct++;
719 $ordered_answers[$answers[$i]] = $_POST['answers'][$row['question_id']][$i];
721 ksort($ordered_answers);
725 // to avoid roundoff errors:
726 if ($num_answer_correct == $num_choices) {
727 $score = $row['weight'];
728 } else if ($num_answer_correct > 0) {
729 $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
730 if ( (float) (int) $score == $score) {
731 $score = (int) $score; // a whole number with decimals, eg. "2.00"
733 $score = trim($score, '0'); // remove trailing zeros, if any, eg. "2.50"
737 $_POST['answers'][$row['question_id']] = implode('|', $ordered_answers);
742 //QTI Import Ordering Question
743 function importQTI($_POST){
746 if ($_POST['question'] == ''){
747 $missing_fields[] = _AT('question');
750 if (trim($_POST['choice'][0]) == '') {
751 $missing_fields[] = _AT('item').' 1';
753 if (trim($_POST['choice'][1]) == '') {
754 $missing_fields[] = _AT('item').' 2';
757 if ($missing_fields) {
758 $missing_fields = implode(', ', $missing_fields);
759 $msg->addError(array('EMPTY_FIELDS', $missing_fields));
762 if (!$msg->containsErrors()) {
763 $choice_new = array(); // stores the non-blank choices
764 $answer_new = array(); // stores the non-blank answers
765 $order = 0; // order count
766 for ($i=0; $i<10; $i++) {
768 * Db defined it to be 255 length, chop strings off it it's less than that
771 $_POST['choice'][$i] = validate_length($_POST['choice'][$i], 255);
772 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
774 if ($_POST['choice'][$i] != '') {
775 /* filter out empty choices/ remove gaps */
776 $choice_new[] = $_POST['choice'][$i];
777 $answer_new[] = $order++;
781 $_POST['choice'] = array_pad($choice_new, 10, '');
782 $answer_new = array_pad($answer_new, 10, 0);
783 // $_POST['feedback'] = $addslashes($_POST['feedback']);
784 // $_POST['question'] = $addslashes($_POST['question']);
786 $sql_params = array( $_POST['category_id'],
787 $_SESSION['course_id'],
811 $sql = vsprintf(AT_SQL_QUESTION_ORDERING, $sql_params);
813 $result = mysql_query($sql, $db);
815 return mysql_insert_id();
825 class TruefalseQuestion extends AbstracttestQuestion {
826 /*protected */ var $sPrefix = 'truefalse';
827 /*protected */ var $sNameVar = 'test_tf';
829 /*protected */function assignQTIVariables($row) {
830 $this->savant->assign('row', $row);
833 /*protected */function assignDisplayResultVariables($row, $answer_row) {
835 $this->savant->assign('base_href', AT_BASE_HREF);
836 $this->savant->assign('answers', $answer_row['answer']);
837 $this->savant->assign('row', $row);
840 /*protected */function assignDisplayVariables($row, $response) {
841 $this->savant->assign('row', $row);
842 $this->savant->assign('response', $response);
845 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
847 foreach ($answers as $answer) {
848 $num_results += $answer['count'];
851 $this->savant->assign('num_results', $num_results);
852 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
853 $this->savant->assign('num_true', (int) $answers['1']['count']);
854 $this->savant->assign('num_false', (int) $answers['2']['count']);
855 $this->savant->assign('row', $row);
858 /*public */function mark($row) {
859 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
861 if ($row['answer_0'] == $_POST['answers'][$row['question_id']]) {
862 return (int) $row['weight'];
867 //QTI Import True/False Question
868 function importQTI($_POST){
871 if ($_POST['question'] == ''){
872 $msg->addError(array('EMPTY_FIELDS', _AT('statement')));
875 //assign true answer to 1, false answer to 2, idk to 3, for ATutor
876 if ($_POST['answer'] == 'ChoiceT'){
877 $_POST['answer'] = 1;
879 $_POST['answer'] = 2;
882 if (!$msg->containsErrors()) {
883 // $_POST['feedback'] = $addslashes($_POST['feedback']);
884 // $_POST['question'] = $addslashes($_POST['question']);
887 $sql_params = array( $_POST['category_id'],
888 $_SESSION['course_id'],
893 $sql = vsprintf(AT_SQL_QUESTION_TRUEFALSE, $sql_params);
894 $result = mysql_query($sql, $db);
896 return mysql_insert_id();
906 class LikertQuestion extends AbstracttestQuestion {
907 /*protected */ var $sPrefix = 'likert';
908 /*protected */ var $sNameVar = 'test_lk';
910 /*protected */function assignQTIVariables($row) {
911 $choices = $this->getChoices($row);
912 $num_choices = count($choices);
914 $this->savant->assign('num_choices', $num_choices);
915 $this->savant->assign('row', $row);
918 /*protected */function assignDisplayResultVariables($row, $answer_row) {
919 $this->savant->assign('answer', $answer_row['answer']);
920 $this->savant->assign('row', $row);
923 /*protected */function assignDisplayVariables($row, $response) {
924 $choices = $this->getChoices($row);
925 $num_choices = count($choices);
927 $this->savant->assign('num_choices', $num_choices);
928 $this->savant->assign('row', $row);
930 if (empty($response)) {
933 $this->savant->assign('response', $response);
936 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
938 foreach ($answers as $answer) {
939 $num_results += $answer['count'];
942 $choices = $this->getChoices($row);
943 $num_choices = count($choices);
946 for ($i=0; $i<$num_choices; $i++) {
947 $sum += ($i+1) * $answers[$i]['count'];
949 $average = round($sum/$num_results, 1);
951 $this->savant->assign('num_results', $num_results);
952 $this->savant->assign('average', $average);
953 $this->savant->assign('num_choices', $num_choices);
954 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
955 $this->savant->assign('answers', $answers);
956 $this->savant->assign('row', $row);
959 /*public */function mark($row) {
960 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
964 //QTI Import Likert Question
965 function importQTI($_POST){
967 // $_POST = $this->_POST;
969 $empty_fields = array();
970 if ($_POST['question'] == ''){
971 $empty_fields[] = _AT('question');
973 if ($_POST['choice'][0] == '') {
974 $empty_fields[] = _AT('choice').' 1';
977 if ($_POST['choice'][1] == '') {
978 $empty_fields[] = _AT('choice').' 2';
981 if (!empty($empty_fields)) {
982 // $msg->addError(array('EMPTY_FIELDS', implode(', ', $empty_fields)));
985 if (!$msg->containsErrors()) {
986 $_POST['feedback'] = '';
987 // $_POST['question'] = $addslashes($_POST['question']);
989 for ($i=0; $i<10; $i++) {
990 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
991 $_POST['answer'][$i] = intval($_POST['answer'][$i]);
993 if ($_POST['choice'][$i] == '') {
994 /* an empty option can't be correct */
995 $_POST['answer'][$i] = 0;
999 $sql_params = array( $_POST['category_id'],
1000 $_SESSION['course_id'],
1003 $_POST['choice'][0],
1004 $_POST['choice'][1],
1005 $_POST['choice'][2],
1006 $_POST['choice'][3],
1007 $_POST['choice'][4],
1008 $_POST['choice'][5],
1009 $_POST['choice'][6],
1010 $_POST['choice'][7],
1011 $_POST['choice'][8],
1012 $_POST['choice'][9],
1013 $_POST['answer'][0],
1014 $_POST['answer'][1],
1015 $_POST['answer'][2],
1016 $_POST['answer'][3],
1017 $_POST['answer'][4],
1018 $_POST['answer'][5],
1019 $_POST['answer'][6],
1020 $_POST['answer'][7],
1021 $_POST['answer'][8],
1022 $_POST['answer'][9]);
1024 $sql = vsprintf(AT_SQL_QUESTION_LIKERT, $sql_params);
1025 $result = mysql_query($sql, $db);
1027 return mysql_insert_id();
1037 class LongQuestion extends AbstracttestQuestion {
1038 /*protected */ var $sPrefix = 'long';
1039 /*protected */ var $sNameVar = 'test_open';
1041 /*protected */function assignQTIVariables($row) {
1042 $this->savant->assign('row', $row);
1045 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1046 $this->savant->assign('answer', $answer_row['answer']);
1047 $this->savant->assign('row', $row);
1050 /*protected */function assignDisplayVariables($row, $response) {
1051 $this->savant->assign('row', $row);
1052 $this->savant->assign('response', $response);
1055 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1057 foreach ($answers as $answer) {
1058 $num_results += $answer['count'];
1061 $this->savant->assign('num_results', $num_results);
1062 $this->savant->assign('num_blanks', (int) $answers['']['count']);
1063 $this->savant->assign('answers', $answers);
1064 $this->savant->assign('row', $row);
1067 /*public */function mark($row) {
1069 $_POST['answers'][$row['question_id']] = $addslashes($_POST['answers'][$row['question_id']]);
1073 //QTI Import Open end/long Question
1074 function importQTI($_POST){
1076 // $_POST = $this->_POST;
1078 if ($_POST['question'] == ''){
1079 // $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1082 if (!$msg->containsErrors()) {
1083 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1084 // $_POST['question'] = $addslashes($_POST['question']);
1086 if ($_POST['property']==''){
1087 $_POST['property'] = 4; //essay
1090 $sql_params = array( $_POST['category_id'],
1091 $_SESSION['course_id'],
1094 $_POST['property']);
1096 $sql = vsprintf(AT_SQL_QUESTION_LONG, $sql_params);
1098 $result = mysql_query($sql, $db);
1100 return mysql_insert_id();
1110 class MatchingQuestion extends AbstracttestQuestion {
1111 /*protected */ var $sPrefix = 'matching';
1112 /*protected */ var $sNameVar = 'test_matching';
1114 /*protected */function assignQTIVariables($row) {
1115 $choices = $this->getChoices($row);
1116 $num_choices = count($choices);
1119 for ($i=0; $i < 10; $i++) {
1120 if ($row['option_'. $i] != '') {
1125 $this->savant->assign('num_choices', $num_choices);
1126 $this->savant->assign('num_options', $num_options);
1127 $this->savant->assign('row', $row);
1130 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1132 for ($i=0; $i < 10; $i++) {
1133 if ($row['option_'. $i] != '') {
1138 $answer_row['answer'] = explode('|', $answer_row['answer']);
1142 $this->savant->assign('base_href', AT_BASE_HREF);
1143 $this->savant->assign('answers', $answer_row['answer']);
1144 $this->savant->assign('letters', $_letters);
1145 $this->savant->assign('num_options', $num_options);
1146 $this->savant->assign('row', $row);
1149 /*protected */function assignDisplayVariables($row, $response) {
1150 $choices = $this->getChoices($row);
1151 $num_choices = count($choices);
1153 if (empty($response)) {
1154 $response = array_fill(0, $num_choices, -1);
1156 $response = explode('|', $response);
1160 for ($i=0; $i < 10; $i++) {
1161 if ($row['option_'. $i] != '') {
1168 $this->savant->assign('num_choices', $num_choices);
1169 $this->savant->assign('base_href', AT_BASE_HREF);
1170 $this->savant->assign('letters', $_letters);
1171 $this->savant->assign('num_options', $num_options);
1172 $this->savant->assign('row', $row);
1174 $this->savant->assign('response', $response);
1177 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1178 $choices = $this->getChoices($row);
1179 $num_choices = count($choices);
1182 foreach ($answers as $answer) {
1183 $num_results += $answer['count'];
1186 foreach ($answers as $key => $value) {
1187 $values = explode('|', $key);
1188 if (count($values) > 1) {
1189 for ($i=0; $i<count($values); $i++) {
1190 $answers[$values[$i]]['count']++;
1195 $this->savant->assign('num_choices', $num_choices);
1196 $this->savant->assign('num_results', $num_results);
1197 $this->savant->assign('answers', $answers);
1198 $this->savant->assign('row', $row);
1201 /*public */function mark($row) {
1202 $num_choices = count($_POST['answers'][$row['question_id']]);
1203 $num_answer_correct = 0;
1204 foreach ($_POST['answers'][$row['question_id']] as $item_id => $response) {
1205 if ($row['answer_' . $item_id] == $response) {
1206 $num_answer_correct++;
1208 $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
1212 // to avoid roundoff errors:
1213 if ($num_answer_correct == $num_choices) {
1214 $score = $row['weight'];
1215 } else if ($num_answer_correct > 0) {
1216 $score = number_format($row['weight'] / $num_choices * $num_answer_correct, 2);
1217 if ( (float) (int) $score == $score) {
1218 $score = (int) $score; // a whole number with decimals, eg. "2.00"
1220 $score = trim($score, '0'); // remove trailing zeros, if any
1224 $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
1229 //QTI Import Matching Question
1230 function importQTI($_POST){
1232 // $_POST = $this->_POST;
1234 if (!is_array($_POST['answer'])){
1235 $temp = $_POST['answer'];
1236 $_POST['answer'] = array();
1237 $_POST['answer'][0] = $temp;
1239 ksort($_POST['answer']); //array_pad returns an array disregard of the array keys
1240 //default for matching is '-'
1241 $_POST['answer']= array_pad($_POST['answer'], 10, -1);
1243 for ($i = 0 ; $i < 10; $i++) {
1244 $_POST['groups'][$i] = trim($_POST['groups'][$i]);
1245 $_POST['answer'][$i] = (int) $_POST['answer'][$i];
1246 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
1249 if (!$_POST['groups'][0]
1250 || !$_POST['groups'][1]
1251 || !$_POST['choice'][0]
1252 || !$_POST['choice'][1]) {
1253 // $msg->addError('QUESTION_EMPTY');
1256 if (!$msg->containsErrors()) {
1257 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1258 // $_POST['instructions'] = $addslashes($_POST['instructions']);
1260 $sql_params = array( $_POST['category_id'],
1261 $_SESSION['course_id'],
1264 $_POST['groups'][0],
1265 $_POST['groups'][1],
1266 $_POST['groups'][2],
1267 $_POST['groups'][3],
1268 $_POST['groups'][4],
1269 $_POST['groups'][5],
1270 $_POST['groups'][6],
1271 $_POST['groups'][7],
1272 $_POST['groups'][8],
1273 $_POST['groups'][9],
1274 $_POST['answer'][0],
1275 $_POST['answer'][1],
1276 $_POST['answer'][2],
1277 $_POST['answer'][3],
1278 $_POST['answer'][4],
1279 $_POST['answer'][5],
1280 $_POST['answer'][6],
1281 $_POST['answer'][7],
1282 $_POST['answer'][8],
1283 $_POST['answer'][9],
1284 $_POST['choice'][0],
1285 $_POST['choice'][1],
1286 $_POST['choice'][2],
1287 $_POST['choice'][3],
1288 $_POST['choice'][4],
1289 $_POST['choice'][5],
1290 $_POST['choice'][6],
1291 $_POST['choice'][7],
1292 $_POST['choice'][8],
1293 $_POST['choice'][9]);
1295 $sql = vsprintf(AT_SQL_QUESTION_MATCHINGDD, $sql_params);
1297 $result = mysql_query($sql, $db);
1299 return mysql_insert_id();
1306 * matchingddQuestion
1309 class MatchingddQuestion extends MatchingQuestion {
1310 /*protected */ var $sPrefix = 'matchingdd';
1311 /*protected */ var $sNameVar = 'test_matchingdd';
1315 * multichoiceQuestion
1318 class MultichoiceQuestion extends AbstracttestQuestion {
1319 /*protected */ var $sPrefix = 'multichoice';
1320 /*protected */var $sNameVar = 'test_mc';
1322 /*protected */function assignQTIVariables($row) {
1323 $choices = $this->getChoices($row);
1324 $num_choices = count($choices);
1326 $this->savant->assign('num_choices', $num_choices);
1327 $this->savant->assign('row', $row);
1330 /*protected */function assignDisplayResultVariables($row, $answer_row) {
1331 if (strpos($answer_row['answer'], '|') !== false) {
1332 $answer_row['answer'] = explode('|', $answer_row['answer']);
1334 $answer_row['answer'] = array($answer_row['answer']);
1337 $this->savant->assign('base_href', AT_BASE_HREF);
1338 $this->savant->assign('answers', $answer_row['answer']);
1339 $this->savant->assign('row', $row);
1342 /*protected */function assignDisplayVariables($row, $response) {
1343 $choices = $this->getChoices($row);
1344 $num_choices = count($choices);
1346 if ($response == '') {
1349 $response = explode('|', $response);
1350 $this->savant->assign('response', $response);
1352 $this->savant->assign('num_choices', $num_choices);
1353 $this->savant->assign('row', $row);
1356 /*protected */function assignDisplayStatisticsVariables($row, $answers) {
1357 $choices = $this->getChoices($row);
1358 $num_choices = count($choices);
1361 foreach ($answers as $answer) {
1362 $num_results += $answer['count'];
1365 foreach ($answers as $key => $value) {
1366 $values = explode('|', $key);
1367 if (count($values) > 1) {
1368 for ($i=0; $i<count($values); $i++) {
1369 $answers[$values[$i]]['count']++;
1374 $this->savant->assign('num_choices', $num_choices);
1375 $this->savant->assign('num_results', $num_results);
1376 $this->savant->assign('num_blanks', (int) $answers['-1']['count']);
1377 $this->savant->assign('answers', $answers);
1378 $this->savant->assign('row', $row);
1381 /*public */function mark($row) {
1383 $_POST['answers'][$row['question_id']] = intval($_POST['answers'][$row['question_id']]);
1384 if ($row['answer_' . $_POST['answers'][$row['question_id']]]) {
1385 $score = $row['weight'];
1386 } else if ($_POST['answers'][$row['question_id']] == -1) {
1388 for($i=0; $i<10; $i++) {
1389 $has_answer += $row['answer_'.$i];
1391 if (!$has_answer && $row['weight']) {
1392 // If MC has no answer and user answered "leave blank"
1393 $score = $row['weight'];
1399 //QTI Import Multiple Choice Question
1400 function importQTI($_POST){
1402 // $_POST = $this->_POST;
1403 if ($_POST['question'] == ''){
1404 $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1407 if (!$msg->containsErrors()) {
1408 // $_POST['question'] = $addslashes($_POST['question']);
1410 for ($i=0; $i<10; $i++) {
1411 $_POST['choice'][$i] = trim($_POST['choice'][$i]);
1414 $answers = array_fill(0, 10, 0);
1415 if (is_array($_POST['answer'])){
1416 $answers[0] = 1; //default the first to be the right answer. TODO, use summation of points.
1418 $answers[$_POST['answer']] = 1;
1421 $sql_params = array( $_POST['category_id'],
1422 $_SESSION['course_id'],
1425 $_POST['choice'][0],
1426 $_POST['choice'][1],
1427 $_POST['choice'][2],
1428 $_POST['choice'][3],
1429 $_POST['choice'][4],
1430 $_POST['choice'][5],
1431 $_POST['choice'][6],
1432 $_POST['choice'][7],
1433 $_POST['choice'][8],
1434 $_POST['choice'][9],
1446 $sql = vsprintf(AT_SQL_QUESTION_MULTI, $sql_params);
1447 $result = mysql_query($sql, $db);
1449 return mysql_insert_id();
1456 * multianswerQuestion
1459 class MultianswerQuestion extends MultichoiceQuestion {
1460 /*protected */ var $sPrefix = 'multianswer';
1461 /*protected */ var $sNameVar = 'test_ma';
1463 /*public */function mark($row) {
1464 $num_correct = array_sum(array_slice($row, 3));
1466 if (is_array($_POST['answers'][$row['question_id']]) && count($_POST['answers'][$row['question_id']]) > 1) {
1467 if (($i = array_search('-1', $_POST['answers'][$row['question_id']])) !== FALSE) {
1468 unset($_POST['answers'][$row['question_id']][$i]);
1470 $num_answer_correct = 0;
1471 foreach ($_POST['answers'][$row['question_id']] as $item_id => $answer) {
1472 if ($row['answer_' . $answer]) {
1474 $num_answer_correct++;
1477 $num_answer_correct--;
1479 $_POST['answers'][$row['question_id']][$item_id] = intval($_POST['answers'][$row['question_id']][$item_id]);
1481 if ($num_answer_correct == $num_correct) {
1482 $score = $row['weight'];
1486 $_POST['answers'][$row['question_id']] = implode('|', $_POST['answers'][$row['question_id']]);
1489 $_POST['answers'][$row['question_id']] = '-1'; // left blank
1495 //QTI Import multianswer Question
1496 function importQTI($_POST){
1498 // $_POST = $this->_POST;
1500 if ($_POST['question'] == ''){
1501 $msg->addError(array('EMPTY_FIELDS', _AT('question')));
1504 //Multiple answer can have 0+ answers, in the QTIImport.class, if size(answer) < 2, answer will be came a scalar.
1505 //The following code will change $_POST[answer] back to a vector.
1506 $_POST['answer'] = $_POST['answers'];
1508 if (!$msg->containsErrors()) {
1509 $choice_new = array(); // stores the non-blank choices
1510 $answer_new = array(); // stores the associated "answer" for the choices
1512 foreach ($_POST['choice'] as $choiceNum=>$choiceOpt) {
1513 $choiceOpt = validate_length($choiceOpt, 255);
1514 $choiceOpt = trim($choiceOpt);
1515 $_POST['answer'][$choiceNum] = intval($_POST['answer'][$choiceNum]);
1516 if ($choiceOpt == '') {
1517 /* an empty option can't be correct */
1518 $_POST['answer'][$choiceNum] = 0;
1520 /* filter out empty choices/ remove gaps */
1521 $choice_new[] = $choiceOpt;
1522 if (in_array($choiceNum, $_POST['answer'])){
1528 if ($_POST['answer'][$choiceNum] != 0)
1533 if ($has_answer != TRUE) {
1535 $hidden_vars['required'] = htmlspecialchars($_POST['required']);
1536 $hidden_vars['feedback'] = htmlspecialchars($_POST['feedback']);
1537 $hidden_vars['question'] = htmlspecialchars($_POST['question']);
1538 $hidden_vars['category_id'] = htmlspecialchars($_POST['category_id']);
1540 for ($i = 0; $i < count($choice_new); $i++) {
1541 $hidden_vars['answer['.$i.']'] = htmlspecialchars($answer_new[$i]);
1542 $hidden_vars['choice['.$i.']'] = htmlspecialchars($choice_new[$i]);
1545 $msg->addConfirm('NO_ANSWER', $hidden_vars);
1547 //add slahes throughout - does that fix it?
1548 $_POST['answer'] = $answer_new;
1549 $_POST['choice'] = $choice_new;
1550 $_POST['answer'] = array_pad($_POST['answer'], 10, 0);
1551 $_POST['choice'] = array_pad($_POST['choice'], 10, '');
1553 // $_POST['feedback'] = $addslashes($_POST['feedback']);
1554 // $_POST['question'] = $addslashes($_POST['question']);
1556 $sql_params = array( $_POST['category_id'],
1557 $_SESSION['course_id'],
1560 $_POST['choice'][0],
1561 $_POST['choice'][1],
1562 $_POST['choice'][2],
1563 $_POST['choice'][3],
1564 $_POST['choice'][4],
1565 $_POST['choice'][5],
1566 $_POST['choice'][6],
1567 $_POST['choice'][7],
1568 $_POST['choice'][8],
1569 $_POST['choice'][9],
1570 $_POST['answer'][0],
1571 $_POST['answer'][1],
1572 $_POST['answer'][2],
1573 $_POST['answer'][3],
1574 $_POST['answer'][4],
1575 $_POST['answer'][5],
1576 $_POST['answer'][6],
1577 $_POST['answer'][7],
1578 $_POST['answer'][8],
1579 $_POST['answer'][9]);
1581 $sql = vsprintf(AT_SQL_QUESTION_MULTIANSWER, $sql_params);
1583 $result = mysql_query($sql, $db);
1585 return mysql_insert_id();